/* * Copyright (c) 2016 The original author or authors * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * and Apache License v2.0 which accompanies this distribution. * * The Eclipse Public License is available at * http://www.eclipse.org/legal/epl-v10.html * * The Apache License v2.0 is available at * http://www.opensource.org/licenses/apache2.0.php * * You may elect to redistribute this code under either of these licenses. */ package io.engagingspaces.vertx.dataloader; import io.vertx.core.CompositeFuture; import io.vertx.core.Future; import io.vertx.core.json.JsonObject; import io.vertx.ext.unit.junit.RunTestOnContext; import io.vertx.ext.unit.junit.VertxUnitRunner; import org.junit.Before; import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import java.util.*; import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import static java.util.Arrays.asList; import static org.awaitility.Awaitility.await; import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertThat; /** * Tests for {@link DataLoader}. * <p> * The tests are a port of the existing tests in * the <a href="https://github.com/facebook/dataloader">facebook/dataloader</a> project. * <p> * Acknowledgments go to <a href="https://github.com/leebyron">Lee Byron</a> for providing excellent coverage. * * @author <a href="https://github.com/aschrijver/">Arnold Schrijver</a> */ @RunWith(VertxUnitRunner.class) public class DataLoaderTest { @Rule public RunTestOnContext rule = new RunTestOnContext(); DataLoader<Integer, Integer> identityLoader; @Before public void setUp() { identityLoader = idLoader(new DataLoaderOptions(), new ArrayList<>()); } @Test public void should_Build_a_really_really_simple_data_loader() { AtomicBoolean success = new AtomicBoolean(); DataLoader<Integer, Integer> identityLoader = new DataLoader<>(keys -> CompositeFuture.join(keys.stream() .map(Future::succeededFuture) .collect(Collectors.toCollection(ArrayList::new)))); Future<Integer> future1 = identityLoader.load(1); future1.setHandler(rh -> { assertThat(rh.result(), equalTo(1)); success.set(rh.succeeded()); }); identityLoader.dispatch(); await().untilAtomic(success, is(true)); } @Test public void should_Support_loading_multiple_keys_in_one_call() { AtomicBoolean success = new AtomicBoolean(); DataLoader<Integer, Integer> identityLoader = new DataLoader<>(keys -> CompositeFuture.join(keys.stream() .map(Future::succeededFuture) .collect(Collectors.toCollection(ArrayList::new)))); CompositeFuture futureAll = identityLoader.loadMany(asList(1, 2)); futureAll.setHandler(rh -> { assertThat(rh.result().size(), is(2)); success.set(rh.succeeded()); }); identityLoader.dispatch(); await().untilAtomic(success, is(true)); assertThat(futureAll.list(), equalTo(asList(1, 2))); } @Test public void should_Resolve_to_empty_list_when_no_keys_supplied() { AtomicBoolean success = new AtomicBoolean(); CompositeFuture futureEmpty = identityLoader.loadMany(Collections.emptyList()); futureEmpty.setHandler(rh -> { assertThat(rh.result().size(), is(0)); success.set(rh.succeeded()); }); identityLoader.dispatch(); await().untilAtomic(success, is(true)); assertThat(futureEmpty.list(), empty()); } @Test public void should_Batch_multiple_requests() { ArrayList<Collection> loadCalls = new ArrayList<>(); DataLoader<Integer, Integer> identityLoader = idLoader(new DataLoaderOptions(), loadCalls); Future<Integer> future1 = identityLoader.load(1); Future<Integer> future2 = identityLoader.load(2); identityLoader.dispatch(); await().until(() -> future1.isComplete() && future2.isComplete()); assertThat(future1.result(), equalTo(1)); assertThat(future2.result(), equalTo(2)); assertThat(loadCalls, equalTo(Collections.singletonList(asList(1, 2)))); } @Test public void should_Coalesce_identical_requests() { ArrayList<Collection> loadCalls = new ArrayList<>(); DataLoader<Integer, Integer> identityLoader = idLoader(new DataLoaderOptions(), loadCalls); Future<Integer> future1a = identityLoader.load(1); Future<Integer> future1b = identityLoader.load(1); assertThat(future1a, equalTo(future1b)); identityLoader.dispatch(); await().until(future1a::isComplete); assertThat(future1a.result(), equalTo(1)); assertThat(future1b.result(), equalTo(1)); assertThat(loadCalls, equalTo(Collections.singletonList(Collections.singletonList(1)))); } @Test public void should_Cache_repeated_requests() { ArrayList<Collection> loadCalls = new ArrayList<>(); DataLoader<String, String> identityLoader = idLoader(new DataLoaderOptions(), loadCalls); Future<String> future1 = identityLoader.load("A"); Future<String> future2 = identityLoader.load("B"); identityLoader.dispatch(); await().until(() -> future1.isComplete() && future2.isComplete()); assertThat(future1.result(), equalTo("A")); assertThat(future2.result(), equalTo("B")); assertThat(loadCalls, equalTo(Collections.singletonList(asList("A", "B")))); Future<String> future1a = identityLoader.load("A"); Future<String> future3 = identityLoader.load("C"); identityLoader.dispatch(); await().until(() -> future1a.isComplete() && future3.isComplete()); assertThat(future1a.result(), equalTo("A")); assertThat(future3.result(), equalTo("C")); assertThat(loadCalls, equalTo(asList(asList("A", "B"), Collections.singletonList("C")))); Future<String> future1b = identityLoader.load("A"); Future<String> future2a = identityLoader.load("B"); Future<String> future3a = identityLoader.load("C"); identityLoader.dispatch(); await().until(() -> future1b.isComplete() && future2a.isComplete() && future3a.isComplete()); assertThat(future1b.result(), equalTo("A")); assertThat(future2a.result(), equalTo("B")); assertThat(future3a.result(), equalTo("C")); assertThat(loadCalls, equalTo(asList(asList("A", "B"), Collections.singletonList("C")))); } @Test public void should_Not_redispatch_previous_load() { ArrayList<Collection> loadCalls = new ArrayList<>(); DataLoader<String, String> identityLoader = idLoader(new DataLoaderOptions(), loadCalls); Future<String> future1 = identityLoader.load("A"); identityLoader.dispatch(); Future<String> future2 = identityLoader.load("B"); identityLoader.dispatch(); await().until(() -> future1.isComplete() && future2.isComplete()); assertThat(future1.result(), equalTo("A")); assertThat(future2.result(), equalTo("B")); assertThat(loadCalls, equalTo(asList(asList("A"), asList("B")))); } @Test public void should_Cache_on_redispatch() { ArrayList<Collection> loadCalls = new ArrayList<>(); DataLoader<String, String> identityLoader = idLoader(new DataLoaderOptions(), loadCalls); Future<String> future1 = identityLoader.load("A"); identityLoader.dispatch(); CompositeFuture future2 = identityLoader.loadMany(asList("A", "B")); identityLoader.dispatch(); await().until(() -> future1.isComplete() && future2.isComplete()); assertThat(future1.result(), equalTo("A")); assertThat(future2.list(), equalTo(asList("A", "B"))); assertThat(loadCalls, equalTo(asList(asList("A"), asList("B")))); } @Test public void should_Clear_single_value_in_loader() { ArrayList<Collection> loadCalls = new ArrayList<>(); DataLoader<String, String> identityLoader = idLoader(new DataLoaderOptions(), loadCalls); Future<String> future1 = identityLoader.load("A"); Future<String> future2 = identityLoader.load("B"); identityLoader.dispatch(); await().until(() -> future1.isComplete() && future2.isComplete()); assertThat(future1.result(), equalTo("A")); assertThat(future2.result(), equalTo("B")); assertThat(loadCalls, equalTo(Collections.singletonList(asList("A", "B")))); identityLoader.clear("A"); Future<String> future1a = identityLoader.load("A"); Future<String> future2a = identityLoader.load("B"); identityLoader.dispatch(); await().until(() -> future1a.isComplete() && future2a.isComplete()); assertThat(future1a.result(), equalTo("A")); assertThat(future2a.result(), equalTo("B")); assertThat(loadCalls, equalTo(asList(asList("A", "B"), Collections.singletonList("A")))); } @Test public void should_Clear_all_values_in_loader() { ArrayList<Collection> loadCalls = new ArrayList<>(); DataLoader<String, String> identityLoader = idLoader(new DataLoaderOptions(), loadCalls); Future<String> future1 = identityLoader.load("A"); Future<String> future2 = identityLoader.load("B"); identityLoader.dispatch(); await().until(() -> future1.isComplete() && future2.isComplete()); assertThat(future1.result(), equalTo("A")); assertThat(future2.result(), equalTo("B")); assertThat(loadCalls, equalTo(Collections.singletonList(asList("A", "B")))); identityLoader.clearAll(); Future<String> future1a = identityLoader.load("A"); Future<String> future2a = identityLoader.load("B"); identityLoader.dispatch(); await().until(() -> future1a.isComplete() && future2a.isComplete()); assertThat(future1a.result(), equalTo("A")); assertThat(future2a.result(), equalTo("B")); assertThat(loadCalls, equalTo(asList(asList("A", "B"), asList("A", "B")))); } @Test public void should_Allow_priming_the_cache() { ArrayList<Collection> loadCalls = new ArrayList<>(); DataLoader<String, String> identityLoader = idLoader(new DataLoaderOptions(), loadCalls); identityLoader.prime("A", "A"); Future<String> future1 = identityLoader.load("A"); Future<String> future2 = identityLoader.load("B"); identityLoader.dispatch(); await().until(() -> future1.isComplete() && future2.isComplete()); assertThat(future1.result(), equalTo("A")); assertThat(future2.result(), equalTo("B")); assertThat(loadCalls, equalTo(Collections.singletonList(Collections.singletonList("B")))); } @Test public void should_Not_prime_keys_that_already_exist() { ArrayList<Collection> loadCalls = new ArrayList<>(); DataLoader<String, String> identityLoader = idLoader(new DataLoaderOptions(), loadCalls); identityLoader.prime("A", "X"); Future<String> future1 = identityLoader.load("A"); Future<String> future2 = identityLoader.load("B"); CompositeFuture composite = identityLoader.dispatch(); await().until((Callable<Boolean>) composite::succeeded); assertThat(future1.result(), equalTo("X")); assertThat(future2.result(), equalTo("B")); identityLoader.prime("A", "Y"); identityLoader.prime("B", "Y"); Future<String> future1a = identityLoader.load("A"); Future<String> future2a = identityLoader.load("B"); CompositeFuture composite2 = identityLoader.dispatch(); await().until((Callable<Boolean>) composite2::succeeded); assertThat(future1a.result(), equalTo("X")); assertThat(future2a.result(), equalTo("B")); assertThat(loadCalls, equalTo(Collections.singletonList(Collections.singletonList("B")))); } @Test public void should_Allow_to_forcefully_prime_the_cache() { ArrayList<Collection> loadCalls = new ArrayList<>(); DataLoader<String, String> identityLoader = idLoader(new DataLoaderOptions(), loadCalls); identityLoader.prime("A", "X"); Future<String> future1 = identityLoader.load("A"); Future<String> future2 = identityLoader.load("B"); CompositeFuture composite = identityLoader.dispatch(); await().until((Callable<Boolean>) composite::succeeded); assertThat(future1.result(), equalTo("X")); assertThat(future2.result(), equalTo("B")); identityLoader.clear("A").prime("A", "Y"); identityLoader.clear("B").prime("B", "Y"); Future<String> future1a = identityLoader.load("A"); Future<String> future2a = identityLoader.load("B"); CompositeFuture composite2 = identityLoader.dispatch(); await().until((Callable<Boolean>) composite2::succeeded); assertThat(future1a.result(), equalTo("Y")); assertThat(future2a.result(), equalTo("Y")); assertThat(loadCalls, equalTo(Collections.singletonList(Collections.singletonList("B")))); } @Test public void should_Resolve_to_error_to_indicate_failure() { ArrayList<Collection> loadCalls = new ArrayList<>(); DataLoader<Integer, Integer> evenLoader = idLoaderWithErrors(new DataLoaderOptions(), loadCalls); Future<Integer> future1 = evenLoader.load(1); evenLoader.dispatch(); await().until(future1::isComplete); assertThat(future1.failed(), is(true)); assertThat(future1.cause(), instanceOf(IllegalStateException.class)); Future<Integer> future2 = evenLoader.load(2); evenLoader.dispatch(); await().until(future2::isComplete); assertThat(future2.result(), equalTo(2)); assertThat(loadCalls, equalTo(asList(Collections.singletonList(1), Collections.singletonList(2)))); } @Test public void should_Represent_failures_and_successes_simultaneously() { AtomicBoolean success = new AtomicBoolean(); ArrayList<Collection> loadCalls = new ArrayList<>(); DataLoader<Integer, Integer> evenLoader = idLoaderWithErrors(new DataLoaderOptions(), loadCalls); Future<Integer> future1 = evenLoader.load(1); Future<Integer> future2 = evenLoader.load(2); Future<Integer> future3 = evenLoader.load(3); Future<Integer> future4 = evenLoader.load(4); CompositeFuture result = evenLoader.dispatch(); result.setHandler(rh -> success.set(true)); await().untilAtomic(success, is(true)); assertThat(future1.failed(), is(true)); assertThat(future1.cause(), instanceOf(IllegalStateException.class)); assertThat(future2.result(), equalTo(2)); assertThat(future3.failed(), is(true)); assertThat(future4.result(), equalTo(4)); assertThat(loadCalls, equalTo(Collections.singletonList(asList(1, 2, 3, 4)))); } @Test public void should_Cache_failed_fetches() { ArrayList<Collection> loadCalls = new ArrayList<>(); DataLoader<Integer, Integer> errorLoader = idLoaderAllErrors(new DataLoaderOptions(), loadCalls); Future<Integer> future1 = errorLoader.load(1); errorLoader.dispatch(); await().until(future1::isComplete); assertThat(future1.failed(), is(true)); assertThat(future1.cause(), instanceOf(IllegalStateException.class)); Future<Integer> future2 = errorLoader.load(1); errorLoader.dispatch(); await().until(future2::isComplete); assertThat(future2.failed(), is(true)); assertThat(future2.cause(), instanceOf(IllegalStateException.class)); assertThat(loadCalls, equalTo(Collections.singletonList(Collections.singletonList(1)))); } @Test public void should_Handle_priming_the_cache_with_an_error() { ArrayList<Collection> loadCalls = new ArrayList<>(); DataLoader<Integer, Integer> identityLoader = idLoader(new DataLoaderOptions(), loadCalls); identityLoader.prime(1, new IllegalStateException("Error")); Future<Integer> future1 = identityLoader.load(1); identityLoader.dispatch(); await().until(future1::isComplete); assertThat(future1.failed(), is(true)); assertThat(future1.cause(), instanceOf(IllegalStateException.class)); assertThat(loadCalls, equalTo(Collections.emptyList())); } @Test public void should_Clear_values_from_cache_after_errors() { ArrayList<Collection> loadCalls = new ArrayList<>(); DataLoader<Integer, Integer> errorLoader = idLoaderAllErrors(new DataLoaderOptions(), loadCalls); Future<Integer> future1 = errorLoader.load(1); future1.setHandler(rh -> { if (rh.failed()) { // Presumably determine if this error is transient, and only clear the cache in that case. errorLoader.clear(1); } }); errorLoader.dispatch(); await().until(future1::isComplete); assertThat(future1.failed(), is(true)); assertThat(future1.cause(), instanceOf(IllegalStateException.class)); Future<Integer> future2 = errorLoader.load(1); future2.setHandler(rh -> { if (rh.failed()) { // Again, only do this if you can determine the error is transient. errorLoader.clear(1); } }); errorLoader.dispatch(); await().until(future2::isComplete); assertThat(future2.failed(), is(true)); assertThat(future2.cause(), instanceOf(IllegalStateException.class)); assertThat(loadCalls, equalTo(asList(Collections.singletonList(1), Collections.singletonList(1)))); } @Test public void should_Propagate_error_to_all_loads() { ArrayList<Collection> loadCalls = new ArrayList<>(); DataLoader<Integer, Integer> errorLoader = idLoaderAllErrors(new DataLoaderOptions(), loadCalls); Future<Integer> future1 = errorLoader.load(1); Future<Integer> future2 = errorLoader.load(2); errorLoader.dispatch(); await().until(future1::isComplete); assertThat(future1.failed(), is(true)); Throwable cause = future1.cause(); assertThat(cause, instanceOf(IllegalStateException.class)); assertThat(cause.getMessage(), equalTo("Error")); await().until(future2::isComplete); cause = future2.cause(); assertThat(cause.getMessage(), equalTo(cause.getMessage())); assertThat(loadCalls, equalTo(Collections.singletonList(asList(1, 2)))); } // Accept any kind of key. @Test public void should_Accept_objects_as_keys() { ArrayList<Collection> loadCalls = new ArrayList<>(); DataLoader<Object, Object> identityLoader = idLoader(new DataLoaderOptions(), loadCalls); Object keyA = new Object(); Object keyB = new Object(); // Fetches as expected identityLoader.load(keyA); identityLoader.load(keyB); identityLoader.dispatch().setHandler(rh -> { assertThat(rh.succeeded(), is(true)); assertThat(rh.result().resultAt(0), equalTo(keyA)); assertThat(rh.result().resultAt(1), equalTo(keyB)); }); assertThat(loadCalls.size(), equalTo(1)); assertThat(loadCalls.get(0).size(), equalTo(2)); assertThat(loadCalls.get(0).toArray()[0], equalTo(keyA)); assertThat(loadCalls.get(0).toArray()[1], equalTo(keyB)); // Caching identityLoader.clear(keyA); //noinspection SuspiciousMethodCalls loadCalls.remove(keyA); identityLoader.load(keyA); identityLoader.load(keyB); identityLoader.dispatch().setHandler(rh -> { assertThat(rh.succeeded(), is(true)); assertThat(rh.result().resultAt(0), equalTo(keyA)); assertThat(identityLoader.getCacheKey(keyB), equalTo(keyB)); }); assertThat(loadCalls.size(), equalTo(2)); assertThat(loadCalls.get(1).size(), equalTo(1)); assertThat(loadCalls.get(1).toArray()[0], equalTo(keyA)); } // Accepts options @Test public void should_Disable_caching() { ArrayList<Collection> loadCalls = new ArrayList<>(); DataLoader<String, String> identityLoader = idLoader(DataLoaderOptions.create().setCachingEnabled(false), loadCalls); Future<String> future1 = identityLoader.load("A"); Future<String> future2 = identityLoader.load("B"); identityLoader.dispatch(); await().until(() -> future1.isComplete() && future2.isComplete()); assertThat(future1.result(), equalTo("A")); assertThat(future2.result(), equalTo("B")); assertThat(loadCalls, equalTo(Collections.singletonList(asList("A", "B")))); Future<String> future1a = identityLoader.load("A"); Future<String> future3 = identityLoader.load("C"); identityLoader.dispatch(); await().until(() -> future1a.isComplete() && future3.isComplete()); assertThat(future1a.result(), equalTo("A")); assertThat(future3.result(), equalTo("C")); assertThat(loadCalls, equalTo(asList(asList("A", "B"), asList("A", "C")))); Future<String> future1b = identityLoader.load("A"); Future<String> future2a = identityLoader.load("B"); Future<String> future3a = identityLoader.load("C"); identityLoader.dispatch(); await().until(() -> future1b.isComplete() && future2a.isComplete() && future3a.isComplete()); assertThat(future1b.result(), equalTo("A")); assertThat(future2a.result(), equalTo("B")); assertThat(future3a.result(), equalTo("C")); assertThat(loadCalls, equalTo(asList(asList("A", "B"), asList("A", "C"), asList("A", "B", "C")))); } // Accepts object key in custom cacheKey function @Test public void should_Accept_objects_with_a_complex_key() { ArrayList<Collection> loadCalls = new ArrayList<>(); DataLoaderOptions options = DataLoaderOptions.create().setCacheKeyFunction(getJsonObjectCacheMapFn()); DataLoader<JsonObject, Integer> identityLoader = idLoader(options, loadCalls); JsonObject key1 = new JsonObject().put("id", 123); JsonObject key2 = new JsonObject().put("id", 123); Future<Integer> future1 = identityLoader.load(key1); Future<Integer> future2 = identityLoader.load(key2); identityLoader.dispatch(); await().until(() -> future1.isComplete() && future2.isComplete()); assertThat(loadCalls, equalTo(Collections.singletonList(Collections.singletonList(key1)))); assertThat(future1.result(), equalTo(key1)); assertThat(future2.result(), equalTo(key1)); } @Test public void should_Clear_objects_with_complex_key() { ArrayList<Collection> loadCalls = new ArrayList<>(); DataLoaderOptions options = DataLoaderOptions.create().setCacheKeyFunction(getJsonObjectCacheMapFn()); DataLoader<JsonObject, Integer> identityLoader = idLoader(options, loadCalls); JsonObject key1 = new JsonObject().put("id", 123); JsonObject key2 = new JsonObject().put("id", 123); Future<Integer> future1 = identityLoader.load(key1); identityLoader.dispatch(); await().until(future1::isComplete); identityLoader.clear(key2); // clear equivalent object key Future<Integer> future2 = identityLoader.load(key1); identityLoader.dispatch(); await().until(future2::isComplete); assertThat(loadCalls, equalTo(asList(Collections.singletonList(key1), Collections.singletonList(key1)))); assertThat(future1.result(), equalTo(key1)); assertThat(future2.result(), equalTo(key1)); } @Test public void should_Accept_objects_with_different_order_of_keys() { ArrayList<Collection> loadCalls = new ArrayList<>(); DataLoaderOptions options = DataLoaderOptions.create().setCacheKeyFunction(getJsonObjectCacheMapFn()); DataLoader<JsonObject, Integer> identityLoader = idLoader(options, loadCalls); JsonObject key1 = new JsonObject().put("a", 123).put("b", 321); JsonObject key2 = new JsonObject().put("b", 321).put("a", 123); // Fetches as expected Future<Integer> future1 = identityLoader.load(key1); Future<Integer> future2 = identityLoader.load(key2); identityLoader.dispatch(); await().until(() -> future1.isComplete() && future2.isComplete()); assertThat(loadCalls, equalTo(Collections.singletonList(Collections.singletonList(key1)))); assertThat(loadCalls.size(), equalTo(1)); assertThat(future1.result(), equalTo(key1)); assertThat(future2.result(), equalTo(key1)); } @Test public void should_Allow_priming_the_cache_with_an_object_key() { ArrayList<Collection> loadCalls = new ArrayList<>(); DataLoaderOptions options = DataLoaderOptions.create().setCacheKeyFunction(getJsonObjectCacheMapFn()); DataLoader<JsonObject, JsonObject> identityLoader = idLoader(options, loadCalls); JsonObject key1 = new JsonObject().put("id", 123); JsonObject key2 = new JsonObject().put("id", 123); identityLoader.prime(key1, key1); Future<JsonObject> future1 = identityLoader.load(key1); Future<JsonObject> future2 = identityLoader.load(key2); identityLoader.dispatch(); await().until(() -> future1.isComplete() && future2.isComplete()); assertThat(loadCalls, equalTo(Collections.emptyList())); assertThat(future1.result(), equalTo(key1)); assertThat(future2.result(), equalTo(key1)); } @Test public void should_Accept_a_custom_cache_map_implementation() { CustomCacheMap customMap = new CustomCacheMap(); ArrayList<Collection> loadCalls = new ArrayList<>(); DataLoaderOptions options = DataLoaderOptions.create().setCacheMap(customMap); DataLoader<String, String> identityLoader = idLoader(options, loadCalls); // Fetches as expected Future future1 = identityLoader.load("a"); Future future2 = identityLoader.load("b"); CompositeFuture composite = identityLoader.dispatch(); await().until((Callable<Boolean>) composite::isComplete); assertThat(future1.result(), equalTo("a")); assertThat(future2.result(), equalTo("b")); assertThat(loadCalls, equalTo(Collections.singletonList(asList("a", "b")))); assertArrayEquals(customMap.stash.keySet().toArray(), asList("a", "b").toArray()); Future future3 = identityLoader.load("c"); Future future2a = identityLoader.load("b"); composite = identityLoader.dispatch(); await().until((Callable<Boolean>) composite::isComplete); assertThat(future3.result(), equalTo("c")); assertThat(future2a.result(), equalTo("b")); assertThat(loadCalls, equalTo(asList(asList("a", "b"), Collections.singletonList("c")))); assertArrayEquals(customMap.stash.keySet().toArray(), asList("a", "b", "c").toArray()); // Supports clear identityLoader.clear("b"); assertArrayEquals(customMap.stash.keySet().toArray(), asList("a", "c").toArray()); Future future2b = identityLoader.load("b"); composite = identityLoader.dispatch(); await().until((Callable<Boolean>) composite::isComplete); assertThat(future2b.result(), equalTo("b")); assertThat(loadCalls, equalTo(asList(asList("a", "b"), Collections.singletonList("c"), Collections.singletonList("b")))); assertArrayEquals(customMap.stash.keySet().toArray(), asList("a", "c", "b").toArray()); // Supports clear all identityLoader.clearAll(); assertArrayEquals(customMap.stash.keySet().toArray(), Collections.emptyList().toArray()); } // It is resilient to job queue ordering @Test public void should_Batch_loads_occurring_within_futures() { ArrayList<Collection> loadCalls = new ArrayList<>(); DataLoader<String, String> identityLoader = idLoader(DataLoaderOptions.create(), loadCalls); Future.<String>future().setHandler(rh -> { identityLoader.load("a"); Future.future().setHandler(rh2 -> { identityLoader.load("b"); Future.future().setHandler(rh3 -> { identityLoader.load("c"); Future.future().setHandler(rh4 -> identityLoader.load("d")).complete(); }).complete(); }).complete(); }).complete(); CompositeFuture composite = identityLoader.dispatch(); await().until((Callable<Boolean>) composite::isComplete); assertThat(loadCalls, equalTo( Collections.singletonList(asList("a", "b", "c", "d")))); } @Test @Ignore public void should_Call_a_loader_from_a_loader() { // TODO Provide implementation with Futures } // Helper methods private static CacheKey<JsonObject> getJsonObjectCacheMapFn() { return key -> key.stream() .map(entry -> entry.getKey() + ":" + entry.getValue()) .sorted() .collect(Collectors.joining()); } public class CustomCacheMap implements CacheMap<String, Object> { public Map<String, Object> stash; public CustomCacheMap() { stash = new LinkedHashMap<>(); } @Override public boolean containsKey(String key) { return stash.containsKey(key); } @Override public Object get(String key) { return stash.get(key); } @Override public CacheMap<String, Object> set(String key, Object value) { stash.put(key, value); return this; } @Override public CacheMap<String, Object> delete(String key) { stash.remove(key); return this; } @Override public CacheMap<String, Object> clear() { stash.clear(); return this; } } @SuppressWarnings("unchecked") private static <K, V> DataLoader<K, V> idLoader(DataLoaderOptions options, List<Collection> loadCalls) { return new DataLoader<>(keys -> { loadCalls.add(new ArrayList(keys)); List<Future> futures = keys.stream().map(Future::succeededFuture).collect(Collectors.toList()); return CompositeFuture.join(futures); }, options); } @SuppressWarnings("unchecked") private static <K, V> DataLoader<K, V> idLoaderAllErrors( DataLoaderOptions options, List<Collection> loadCalls) { return new DataLoader<>(keys -> { loadCalls.add(new ArrayList(keys)); List<Future> futures = keys.stream() .map(key -> Future.failedFuture(new IllegalStateException("Error"))) .collect(Collectors.toList()); return CompositeFuture.join(futures); }, options); } @SuppressWarnings("unchecked") private static DataLoader<Integer, Integer> idLoaderWithErrors( DataLoaderOptions options, List<Collection> loadCalls) { return new DataLoader<>(keys -> { loadCalls.add(new ArrayList(keys)); List<Future> futures = keys.stream() .map(key -> key % 2 == 0 ? Future.succeededFuture(key) : Future.failedFuture(new IllegalStateException("Error"))) .collect(Collectors.toList()); return CompositeFuture.join(futures); }, options); } }