diff --git a/README.md b/README.md index 0775e2f..bfb2bd9 100644 --- a/README.md +++ b/README.md @@ -402,7 +402,9 @@ then you will not want to cache data meant for user A to then later give it user The scope of your `DataLoader` instances is important. You might want to create them per web request to ensure data is only cached within that web request and no more. -If your data can be shared across web requests then you might want to scope your data loaders so they survive longer than the web request say. +If your data can be shared across web requests then use a custom cache to keep values in a common place. You should however aim +to keep the data loader instances per web request because they are stateful components that contain promises (with context) +that are likely share the same affinity as the web request. ## Custom caches @@ -461,7 +463,7 @@ repositories { } dependencies { - compile 'com.graphql-java:java-dataloader: 2.2.3' + compile 'com.graphql-java:java-dataloader: 3.0.0' } ``` diff --git a/src/main/java/org/dataloader/BatchLoader.java b/src/main/java/org/dataloader/BatchLoader.java index aee9df2..fed2baf 100644 --- a/src/main/java/org/dataloader/BatchLoader.java +++ b/src/main/java/org/dataloader/BatchLoader.java @@ -16,6 +16,8 @@ package org.dataloader; +import org.dataloader.annotations.PublicSpi; + import java.util.List; import java.util.concurrent.CompletionStage; diff --git a/src/main/java/org/dataloader/BatchLoaderContextProvider.java b/src/main/java/org/dataloader/BatchLoaderContextProvider.java index 0eda7cc..d1eb1fe 100644 --- a/src/main/java/org/dataloader/BatchLoaderContextProvider.java +++ b/src/main/java/org/dataloader/BatchLoaderContextProvider.java @@ -1,5 +1,7 @@ package org.dataloader; +import org.dataloader.annotations.PublicSpi; + /** * A BatchLoaderContextProvider is used by the {@link org.dataloader.DataLoader} code to * provide overall calling context to the {@link org.dataloader.BatchLoader} call. A common use diff --git a/src/main/java/org/dataloader/BatchLoaderEnvironment.java b/src/main/java/org/dataloader/BatchLoaderEnvironment.java index 096c05a..8ab0f39 100644 --- a/src/main/java/org/dataloader/BatchLoaderEnvironment.java +++ b/src/main/java/org/dataloader/BatchLoaderEnvironment.java @@ -1,5 +1,7 @@ package org.dataloader; +import org.dataloader.annotations.PublicApi; + import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -7,6 +9,7 @@ import java.util.Map; import static java.util.Objects.nonNull; +import static java.util.Objects.requireNonNull; /** * This object is passed to a batch loader as calling context. It could contain security credentials @@ -78,8 +81,8 @@ public Builder context(Object context) { } public Builder keyContexts(List keys, List keyContexts) { - nonNull(keys); - nonNull(keyContexts); + requireNonNull(keys); + requireNonNull(keyContexts); Map map = new HashMap<>(); List list = new ArrayList<>(); diff --git a/src/main/java/org/dataloader/BatchLoaderEnvironmentProvider.java b/src/main/java/org/dataloader/BatchLoaderEnvironmentProvider.java index 66d46c1..fd60a14 100644 --- a/src/main/java/org/dataloader/BatchLoaderEnvironmentProvider.java +++ b/src/main/java/org/dataloader/BatchLoaderEnvironmentProvider.java @@ -1,5 +1,7 @@ package org.dataloader; +import org.dataloader.annotations.PublicSpi; + /** * A BatchLoaderEnvironmentProvider is used by the {@link org.dataloader.DataLoader} code to * provide {@link org.dataloader.BatchLoaderEnvironment} calling context to diff --git a/src/main/java/org/dataloader/BatchLoaderWithContext.java b/src/main/java/org/dataloader/BatchLoaderWithContext.java index b82a63f..fbe66b0 100644 --- a/src/main/java/org/dataloader/BatchLoaderWithContext.java +++ b/src/main/java/org/dataloader/BatchLoaderWithContext.java @@ -1,5 +1,7 @@ package org.dataloader; +import org.dataloader.annotations.PublicSpi; + import java.util.List; import java.util.concurrent.CompletionStage; diff --git a/src/main/java/org/dataloader/CacheMap.java b/src/main/java/org/dataloader/CacheMap.java index f60c6ef..297cd0a 100644 --- a/src/main/java/org/dataloader/CacheMap.java +++ b/src/main/java/org/dataloader/CacheMap.java @@ -16,10 +16,9 @@ package org.dataloader; +import org.dataloader.annotations.PublicSpi; import org.dataloader.impl.DefaultCacheMap; -import java.util.concurrent.CompletableFuture; - /** * Cache map interface for data loaders that use caching. *

@@ -31,7 +30,6 @@ * * @param type parameter indicating the type of the cache keys * @param type parameter indicating the type of the data that is cached - * * @author Arnold Schrijver * @author Brad Baker */ @@ -43,10 +41,9 @@ public interface CacheMap { * * @param type parameter indicating the type of the cache keys * @param type parameter indicating the type of the data that is cached - * * @return the cache map */ - static CacheMap> simpleMap() { + static CacheMap simpleMap() { return new DefaultCacheMap<>(); } @@ -54,7 +51,6 @@ static CacheMap> simpleMap() { * Checks whether the specified key is contained in the cach map. * * @param key the key to check - * * @return {@code true} if the cache contains the key, {@code false} otherwise */ boolean containsKey(U key); @@ -66,7 +62,6 @@ static CacheMap> simpleMap() { * so be sure to check {@link CacheMap#containsKey(Object)} first. * * @param key the key to retrieve - * * @return the cached value, or {@code null} if not found (depends on cache implementation) */ V get(U key); @@ -76,7 +71,6 @@ static CacheMap> simpleMap() { * * @param key the key to cache * @param value the value to cache - * * @return the cache map for fluent coding */ CacheMap set(U key, V value); @@ -85,7 +79,6 @@ static CacheMap> simpleMap() { * Deletes the entry with the specified key from the cache map, if it exists. * * @param key the key to delete - * * @return the cache map for fluent coding */ CacheMap delete(U key); diff --git a/src/main/java/org/dataloader/DataLoader.java b/src/main/java/org/dataloader/DataLoader.java index a088070..44a3d79 100644 --- a/src/main/java/org/dataloader/DataLoader.java +++ b/src/main/java/org/dataloader/DataLoader.java @@ -1,33 +1,12 @@ -/* - * 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 org.dataloader; -import org.dataloader.impl.CompletableFutureKit; +import org.dataloader.annotations.PublicApi; import org.dataloader.stats.Statistics; -import org.dataloader.stats.StatisticsCollector; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; -import static org.dataloader.impl.Assertions.nonNull; - /** * Data loader is a utility class that allows batch loading of data that is identified by a set of unique keys. For * each key that is loaded a separate {@link CompletableFuture} is returned, that completes as the batch function completes. @@ -46,8 +25,9 @@ *

* A call to the batch loader might result in individual exception failures for item with the returned list. if * you want to capture these specific item failures then use {@link org.dataloader.Try} as a return value and - * create the data loader with {@link #newDataLoaderWithTry(BatchLoader)} form. The Try values will be interpreted - * as either success values or cause the {@link #load(Object)} promise to complete exceptionally. + * create the data loader with {@link org.dataloader.DataLoaderFactory#newDataLoaderWithTry(BatchLoader)} form. + * The Try values will be interpreted as either success values or cause the {@link #load(Object)} promise to + * complete exceptionally. * * @param type parameter indicating the type of the data load keys * @param type parameter indicating the type of the data that is returned @@ -55,352 +35,23 @@ * @author Brad Baker */ @PublicApi -public class DataLoader { - - private final DataLoaderHelper helper; - private final DataLoaderOptions loaderOptions; - private final CacheMap> futureCache; - private final StatisticsCollector stats; - - /** - * Creates new DataLoader with the specified batch loader function and default options - * (batching, caching and unlimited batch size). - * - * @param batchLoadFunction the batch load function to use - * @param the key type - * @param the value type - * @return a new DataLoader - */ - public static DataLoader newDataLoader(BatchLoader batchLoadFunction) { - return newDataLoader(batchLoadFunction, null); - } - - /** - * Creates new DataLoader with the specified batch loader function with the provided options - * - * @param batchLoadFunction the batch load function to use - * @param options the options to use - * @param the key type - * @param the value type - * @return a new DataLoader - */ - public static DataLoader newDataLoader(BatchLoader batchLoadFunction, DataLoaderOptions options) { - return new DataLoader<>(batchLoadFunction, options); - } - - /** - * Creates new DataLoader with the specified batch loader function and default options - * (batching, caching and unlimited batch size) where the batch loader function returns a list of - * {@link org.dataloader.Try} objects. - *

- * If its important you to know the exact status of each item in a batch call and whether it threw exceptions then - * you can use this form to create the data loader. - *

- * Using Try objects allows you to capture a value returned or an exception that might - * have occurred trying to get a value. . - * - * @param batchLoadFunction the batch load function to use that uses {@link org.dataloader.Try} objects - * @param the key type - * @param the value type - * @return a new DataLoader - */ - public static DataLoader newDataLoaderWithTry(BatchLoader> batchLoadFunction) { - return newDataLoaderWithTry(batchLoadFunction, null); - } - - /** - * Creates new DataLoader with the specified batch loader function and with the provided options - * where the batch loader function returns a list of - * {@link org.dataloader.Try} objects. - * - * @param batchLoadFunction the batch load function to use that uses {@link org.dataloader.Try} objects - * @param options the options to use - * @param the key type - * @param the value type - * @return a new DataLoader - * @see #newDataLoaderWithTry(BatchLoader) - */ - @SuppressWarnings("unchecked") - public static DataLoader newDataLoaderWithTry(BatchLoader> batchLoadFunction, DataLoaderOptions options) { - return new DataLoader<>((BatchLoader) batchLoadFunction, options); - } - - /** - * Creates new DataLoader with the specified batch loader function and default options - * (batching, caching and unlimited batch size). - * - * @param batchLoadFunction the batch load function to use - * @param the key type - * @param the value type - * @return a new DataLoader - */ - public static DataLoader newDataLoader(BatchLoaderWithContext batchLoadFunction) { - return newDataLoader(batchLoadFunction, null); - } - - /** - * Creates new DataLoader with the specified batch loader function with the provided options - * - * @param batchLoadFunction the batch load function to use - * @param options the options to use - * @param the key type - * @param the value type - * @return a new DataLoader - */ - public static DataLoader newDataLoader(BatchLoaderWithContext batchLoadFunction, DataLoaderOptions options) { - return new DataLoader<>(batchLoadFunction, options); - } - - /** - * Creates new DataLoader with the specified batch loader function and default options - * (batching, caching and unlimited batch size) where the batch loader function returns a list of - * {@link org.dataloader.Try} objects. - *

- * If its important you to know the exact status of each item in a batch call and whether it threw exceptions then - * you can use this form to create the data loader. - *

- * Using Try objects allows you to capture a value returned or an exception that might - * have occurred trying to get a value. . - * - * @param batchLoadFunction the batch load function to use that uses {@link org.dataloader.Try} objects - * @param the key type - * @param the value type - * @return a new DataLoader - */ - public static DataLoader newDataLoaderWithTry(BatchLoaderWithContext> batchLoadFunction) { - return newDataLoaderWithTry(batchLoadFunction, null); - } - - /** - * Creates new DataLoader with the specified batch loader function and with the provided options - * where the batch loader function returns a list of - * {@link org.dataloader.Try} objects. - * - * @param batchLoadFunction the batch load function to use that uses {@link org.dataloader.Try} objects - * @param options the options to use - * @param the key type - * @param the value type - * @return a new DataLoader - * @see #newDataLoaderWithTry(BatchLoader) - */ - public static DataLoader newDataLoaderWithTry(BatchLoaderWithContext> batchLoadFunction, DataLoaderOptions options) { - return new DataLoader<>(batchLoadFunction, options); - } - - /** - * Creates new DataLoader with the specified batch loader function and default options - * (batching, caching and unlimited batch size). - * - * @param batchLoadFunction the batch load function to use - * @param the key type - * @param the value type - * @return a new DataLoader - */ - public static DataLoader newMappedDataLoader(MappedBatchLoader batchLoadFunction) { - return newMappedDataLoader(batchLoadFunction, null); - } - - /** - * Creates new DataLoader with the specified batch loader function with the provided options - * - * @param batchLoadFunction the batch load function to use - * @param options the options to use - * @param the key type - * @param the value type - * @return a new DataLoader - */ - public static DataLoader newMappedDataLoader(MappedBatchLoader batchLoadFunction, DataLoaderOptions options) { - return new DataLoader<>(batchLoadFunction, options); - } - - /** - * Creates new DataLoader with the specified batch loader function and default options - * (batching, caching and unlimited batch size) where the batch loader function returns a list of - * {@link org.dataloader.Try} objects. - *

- * If its important you to know the exact status of each item in a batch call and whether it threw exceptions then - * you can use this form to create the data loader. - *

- * Using Try objects allows you to capture a value returned or an exception that might - * have occurred trying to get a value. . - *

- * - * @param batchLoadFunction the batch load function to use that uses {@link org.dataloader.Try} objects - * @param the key type - * @param the value type - * @return a new DataLoader - */ - public static DataLoader newMappedDataLoaderWithTry(MappedBatchLoader> batchLoadFunction) { - return newMappedDataLoaderWithTry(batchLoadFunction, null); - } - - /** - * Creates new DataLoader with the specified batch loader function and with the provided options - * where the batch loader function returns a list of - * {@link org.dataloader.Try} objects. - * - * @param batchLoadFunction the batch load function to use that uses {@link org.dataloader.Try} objects - * @param options the options to use - * @param the key type - * @param the value type - * @return a new DataLoader - * @see #newDataLoaderWithTry(BatchLoader) - */ - @SuppressWarnings("unchecked") - public static DataLoader newMappedDataLoaderWithTry(MappedBatchLoader> batchLoadFunction, DataLoaderOptions options) { - return new DataLoader<>(batchLoadFunction, options); - } - - /** - * Creates new DataLoader with the specified mapped batch loader function and default options - * (batching, caching and unlimited batch size). - * - * @param batchLoadFunction the batch load function to use - * @param the key type - * @param the value type - * @return a new DataLoader - */ - public static DataLoader newMappedDataLoader(MappedBatchLoaderWithContext batchLoadFunction) { - return newMappedDataLoader(batchLoadFunction, null); - } - - /** - * Creates new DataLoader with the specified batch loader function with the provided options - * - * @param batchLoadFunction the batch load function to use - * @param options the options to use - * @param the key type - * @param the value type - * @return a new DataLoader - */ - public static DataLoader newMappedDataLoader(MappedBatchLoaderWithContext batchLoadFunction, DataLoaderOptions options) { - return new DataLoader<>(batchLoadFunction, options); - } - - /** - * Creates new DataLoader with the specified batch loader function and default options - * (batching, caching and unlimited batch size) where the batch loader function returns a list of - * {@link org.dataloader.Try} objects. - *

- * If its important you to know the exact status of each item in a batch call and whether it threw exceptions then - * you can use this form to create the data loader. - *

- * Using Try objects allows you to capture a value returned or an exception that might - * have occurred trying to get a value. . - * - * @param batchLoadFunction the batch load function to use that uses {@link org.dataloader.Try} objects - * @param the key type - * @param the value type - * @return a new DataLoader - */ - public static DataLoader newMappedDataLoaderWithTry(MappedBatchLoaderWithContext> batchLoadFunction) { - return newMappedDataLoaderWithTry(batchLoadFunction, null); - } - - /** - * Creates new DataLoader with the specified batch loader function and with the provided options - * where the batch loader function returns a list of - * {@link org.dataloader.Try} objects. - * - * @param batchLoadFunction the batch load function to use that uses {@link org.dataloader.Try} objects - * @param options the options to use - * @param the key type - * @param the value type - * @return a new DataLoader - * @see #newDataLoaderWithTry(BatchLoader) - */ - @SuppressWarnings("unchecked") - public static DataLoader newMappedDataLoaderWithTry(MappedBatchLoaderWithContext> batchLoadFunction, DataLoaderOptions options) { - return new DataLoader<>(batchLoadFunction, options); - } - - /** - * Creates a new data loader with the provided batch load function, and default options. - * - * @param batchLoadFunction the batch load function to use - */ - public DataLoader(BatchLoader batchLoadFunction) { - this(batchLoadFunction, null); - } - - /** - * Creates a new data loader with the provided batch load function and options. - * - * @param batchLoadFunction the batch load function to use - * @param options the batch load options - */ - public DataLoader(BatchLoader batchLoadFunction, DataLoaderOptions options) { - this((Object) batchLoadFunction, options); - } - - private DataLoader(Object batchLoadFunction, DataLoaderOptions options) { - this.loaderOptions = options == null ? new DataLoaderOptions() : options; - this.futureCache = determineCacheMap(loaderOptions); - // order of keys matter in data loader - this.stats = nonNull(this.loaderOptions.getStatisticsCollector()); - - this.helper = new DataLoaderHelper<>(this, batchLoadFunction, this.loaderOptions, this.futureCache, this.stats); - } - - @SuppressWarnings("unchecked") - private CacheMap> determineCacheMap(DataLoaderOptions loaderOptions) { - return loaderOptions.cacheMap().isPresent() ? (CacheMap>) loaderOptions.cacheMap().get() : CacheMap.simpleMap(); - } - +public interface DataLoader { /** * Requests to load the data with the specified key asynchronously, and returns a future of the resulting value. *

- * If batching is enabled (the default), you'll have to call {@link DataLoader#dispatch()} at a later stage to + * If batching is enabled (the default), you'll have to call {@link org.dataloader.DataLoader#dispatch()} at a later stage to * start batch execution. If you forget this call the future will never be completed (unless already completed, * and returned from cache). * * @param key the key to load * @return the future of the value */ - public CompletableFuture load(K key) { - return load(key, null); - } - - /** - * This will return an optional promise to a value previously loaded via a {@link #load(Object)} call or empty if not call has been made for that key. - *

- * If you do get a present CompletableFuture it does not mean it has been dispatched and completed yet. It just means - * its at least pending and in cache. - *

- * If caching is disabled there will never be a present Optional returned. - *

- * NOTE : This will NOT cause a data load to happen. You must called {@link #load(Object)} for that to happen. - * - * @param key the key to check - * @return an Optional to the future of the value - */ - public Optional> getIfPresent(K key) { - return helper.getIfPresent(key); - } - - /** - * This will return an optional promise to a value previously loaded via a {@link #load(Object)} call that has in fact been completed or empty - * if no call has been made for that key or the promise has not completed yet. - *

- * If you do get a present CompletableFuture it means it has been dispatched and completed. Completed is defined as - * {@link java.util.concurrent.CompletableFuture#isDone()} returning true. - *

- * If caching is disabled there will never be a present Optional returned. - *

- * NOTE : This will NOT cause a data load to happen. You must called {@link #load(Object)} for that to happen. - * - * @param key the key to check - * @return an Optional to the future of the value - */ - public Optional> getIfCompleted(K key) { - return helper.getIfCompleted(key); - } - + CompletableFuture load(K key); /** * Requests to load the data with the specified key asynchronously, and returns a future of the resulting value. *

- * If batching is enabled (the default), you'll have to call {@link DataLoader#dispatch()} at a later stage to + * If batching is enabled (the default), you'll have to call {@link org.dataloader.DataLoader#dispatch()} at a later stage to * start batch execution. If you forget this call the future will never be completed (unless already completed, * and returned from cache). *

@@ -411,30 +62,26 @@ public Optional> getIfCompleted(K key) { * @param keyContext a context object that is specific to this key * @return the future of the value */ - public CompletableFuture load(K key, Object keyContext) { - return helper.load(key, keyContext); - } + CompletableFuture load(K key, Object keyContext); /** * Requests to load the list of data provided by the specified keys asynchronously, and returns a composite future * of the resulting values. *

- * If batching is enabled (the default), you'll have to call {@link DataLoader#dispatch()} at a later stage to + * If batching is enabled (the default), you'll have to call {@link org.dataloader.DataLoader#dispatch()} at a later stage to * start batch execution. If you forget this call the future will never be completed (unless already completed, * and returned from cache). * * @param keys the list of keys to load * @return the composite future of the list of values */ - public CompletableFuture> loadMany(List keys) { - return loadMany(keys, Collections.emptyList()); - } + CompletableFuture> loadMany(List keys); /** * Requests to load the list of data provided by the specified keys asynchronously, and returns a composite future * of the resulting values. *

- * If batching is enabled (the default), you'll have to call {@link DataLoader#dispatch()} at a later stage to + * If batching is enabled (the default), you'll have to call {@link org.dataloader.DataLoader#dispatch()} at a later stage to * start batch execution. If you forget this call the future will never be completed (unless already completed, * and returned from cache). *

@@ -445,37 +92,42 @@ public CompletableFuture> loadMany(List keys) { * @param keyContexts the list of key calling context objects * @return the composite future of the list of values */ - public CompletableFuture> loadMany(List keys, List keyContexts) { - nonNull(keys); - nonNull(keyContexts); + CompletableFuture> loadMany(List keys, List keyContexts); - synchronized (this) { - List> collect = new ArrayList<>(); - for (int i = 0; i < keys.size(); i++) { - K key = keys.get(i); - Object keyContext = null; - if (i < keyContexts.size()) { - keyContext = keyContexts.get(i); - } - collect.add(load(key, keyContext)); - } - return CompletableFutureKit.allOf(collect); - } - } + /** + * This will return an optional promise to a value previously loaded via a {@link #load(Object)} call or empty if not call has been made for that key. + *

+ * If you do get a present CompletableFuture it does not mean it has been dispatched and completed yet. It just means + * its at least pending and in cache. + *

+ * If caching is disabled there will never be a present Optional returned. + *

+ * NOTE : This will NOT cause a data load to happen. You must called {@link #load(Object)} for that to happen. + * + * @param key the key to check + * @return an Optional to the future of the value + */ + Optional> getIfPresent(K key); /** - * Dispatches the queued load requests to the batch execution function and returns a promise of the result. + * This will return an optional promise to a value previously loaded via a {@link #load(Object)} call that has in fact been completed or empty + * if no call has been made for that key or the promise has not completed yet. + *

+ * If you do get a present CompletableFuture it means it has been dispatched and completed. Completed is defined as + * {@link java.util.concurrent.CompletableFuture#isDone()} returning true. + *

+ * If caching is disabled there will never be a present Optional returned. *

- * If batching is disabled, or there are no queued requests, then a succeeded promise is returned. + * NOTE : This will NOT cause a data load to happen. You must called {@link #load(Object)} for that to happen. * - * @return the promise of the queued load requests + * @param key the key to check + * @return an Optional to the future of the value */ - public CompletableFuture> dispatch() { - return helper.dispatch().getPromisedResults(); - } + Optional> getIfCompleted(K key); + /** - * Dispatches the queued load requests to the batch execution function and returns both the promise of the result + * Dispatches the queued load requests to the batch execution function and returns both the promise of the results * and the number of entries that were dispatched. *

* If batching is disabled, or there are no queued requests, then a succeeded promise with no entries dispatched is @@ -483,9 +135,7 @@ public CompletableFuture> dispatch() { * * @return the promise of the queued load requests and the number of keys dispatched. */ - public DispatchResult dispatchWithCounts() { - return helper.dispatch(); - } + DispatchResult dispatch(); /** * Normally {@link #dispatch()} is an asynchronous operation but this version will 'join' on the @@ -495,25 +145,20 @@ public DispatchResult dispatchWithCounts() { * * @return the list of all results when the {@link #dispatchDepth()} reached 0 */ - public List dispatchAndJoin() { - List results = new ArrayList<>(); - - List joinedResults = dispatch().join(); - results.addAll(joinedResults); - while (this.dispatchDepth() > 0) { - joinedResults = dispatch().join(); - results.addAll(joinedResults); - } - return results; - } - + List dispatchAndJoin(); /** * @return the depth of the batched key loads that need to be dispatched */ - public int dispatchDepth() { - return helper.dispatchDepth(); - } + int dispatchDepth(); + + /** + * Gets the statistics associated with this data loader. These will have been gather via + * the {@link org.dataloader.stats.StatisticsCollector} passed in via {@link DataLoaderOptions#getStatisticsCollector()} + * + * @return statistics for this data loader + */ + Statistics getStatistics(); /** @@ -523,25 +168,14 @@ public int dispatchDepth() { * @param key the key to remove * @return the data loader for fluent coding */ - public DataLoader clear(K key) { - Object cacheKey = getCacheKey(key); - synchronized (this) { - futureCache.delete(cacheKey); - } - return this; - } + DataLoader clear(K key); /** * Clears the entire cache map of the loader. * * @return the data loader for fluent coding */ - public DataLoader clearAll() { - synchronized (this) { - futureCache.clear(); - } - return this; - } + DataLoader clearAll(); /** * Primes the cache with the given key and value. @@ -550,15 +184,7 @@ public DataLoader clearAll() { * @param value the value * @return the data loader for fluent coding */ - public DataLoader prime(K key, V value) { - Object cacheKey = getCacheKey(key); - synchronized (this) { - if (!futureCache.containsKey(cacheKey)) { - futureCache.set(cacheKey, CompletableFuture.completedFuture(value)); - } - } - return this; - } + DataLoader prime(K key, V value); /** * Primes the cache with the given key and error. @@ -567,13 +193,7 @@ public DataLoader prime(K key, V value) { * @param error the exception to prime instead of a value * @return the data loader for fluent coding */ - public DataLoader prime(K key, Exception error) { - Object cacheKey = getCacheKey(key); - if (!futureCache.containsKey(cacheKey)) { - futureCache.set(cacheKey, CompletableFutureKit.failedFuture(error)); - } - return this; - } + DataLoader prime(K key, Exception error); /** * Gets the object that is used in the internal cache map as key, by applying the cache key function to @@ -584,18 +204,6 @@ public DataLoader prime(K key, Exception error) { * @param key the input key * @return the cache key after the input is transformed with the cache key function */ - public Object getCacheKey(K key) { - return helper.getCacheKey(key); - } - - /** - * Gets the statistics associated with this data loader. These will have been gather via - * the {@link org.dataloader.stats.StatisticsCollector} passed in via {@link DataLoaderOptions#getStatisticsCollector()} - * - * @return statistics for this data loader - */ - public Statistics getStatistics() { - return stats.getStatistics(); - } + Object getCacheKey(K key); } diff --git a/src/main/java/org/dataloader/DataLoaderFactory.java b/src/main/java/org/dataloader/DataLoaderFactory.java new file mode 100644 index 0000000..dc27d27 --- /dev/null +++ b/src/main/java/org/dataloader/DataLoaderFactory.java @@ -0,0 +1,260 @@ +package org.dataloader; + + +import org.dataloader.impl.DataLoaderImpl; + +/** + * This factory creates instances of {@link org.dataloader.DataLoader}s. + */ +public class DataLoaderFactory { + + /** + * Creates new DataLoader with the specified batch loader function and default options + * (batching, caching and unlimited batch size). + * + * @param batchLoadFunction the batch load function to use + * @param the key type + * @param the value type + * @return a new DataLoader + */ + public static DataLoader newDataLoader(BatchLoader batchLoadFunction) { + return newDataLoader(batchLoadFunction, null); + } + + /** + * Creates new DataLoader with the specified batch loader function with the provided options + * + * @param batchLoadFunction the batch load function to use + * @param options the options to use + * @param the key type + * @param the value type + * @return a new DataLoader + */ + public static DataLoader newDataLoader(BatchLoader batchLoadFunction, DataLoaderOptions options) { + return new DataLoaderImpl<>(batchLoadFunction, options); + } + + /** + * Creates new DataLoader with the specified batch loader function and default options + * (batching, caching and unlimited batch size) where the batch loader function returns a list of + * {@link org.dataloader.Try} objects. + *

+ * If its important you to know the exact status of each item in a batch call and whether it threw exceptions then + * you can use this form to create the data loader. + *

+ * Using Try objects allows you to capture a value returned or an exception that might + * have occurred trying to get a value. . + * + * @param batchLoadFunction the batch load function to use that uses {@link org.dataloader.Try} objects + * @param the key type + * @param the value type + * @return a new DataLoader + */ + public static DataLoader newDataLoaderWithTry(BatchLoader> batchLoadFunction) { + return newDataLoaderWithTry(batchLoadFunction, null); + } + + /** + * Creates new DataLoader with the specified batch loader function and with the provided options + * where the batch loader function returns a list of + * {@link org.dataloader.Try} objects. + * + * @param batchLoadFunction the batch load function to use that uses {@link org.dataloader.Try} objects + * @param options the options to use + * @param the key type + * @param the value type + * @return a new DataLoader + * @see #newDataLoaderWithTry(BatchLoader) + */ + public static DataLoader newDataLoaderWithTry(BatchLoader> batchLoadFunction, DataLoaderOptions options) { + return new DataLoaderImpl<>(batchLoadFunction, options); + } + + /** + * Creates new DataLoader with the specified batch loader function and default options + * (batching, caching and unlimited batch size). + * + * @param batchLoadFunction the batch load function to use + * @param the key type + * @param the value type + * @return a new DataLoader + */ + public static DataLoader newDataLoader(BatchLoaderWithContext batchLoadFunction) { + return newDataLoader(batchLoadFunction, null); + } + + /** + * Creates new DataLoader with the specified batch loader function with the provided options + * + * @param batchLoadFunction the batch load function to use + * @param options the options to use + * @param the key type + * @param the value type + * @return a new DataLoader + */ + public static DataLoader newDataLoader(BatchLoaderWithContext batchLoadFunction, DataLoaderOptions options) { + return new DataLoaderImpl<>(batchLoadFunction, options); + } + + /** + * Creates new DataLoader with the specified batch loader function and default options + * (batching, caching and unlimited batch size) where the batch loader function returns a list of + * {@link org.dataloader.Try} objects. + *

+ * If its important you to know the exact status of each item in a batch call and whether it threw exceptions then + * you can use this form to create the data loader. + *

+ * Using Try objects allows you to capture a value returned or an exception that might + * have occurred trying to get a value. . + * + * @param batchLoadFunction the batch load function to use that uses {@link org.dataloader.Try} objects + * @param the key type + * @param the value type + * @return a new DataLoader + */ + public static DataLoader newDataLoaderWithTry(BatchLoaderWithContext> batchLoadFunction) { + return newDataLoaderWithTry(batchLoadFunction, null); + } + + /** + * Creates new DataLoader with the specified batch loader function and with the provided options + * where the batch loader function returns a list of + * {@link org.dataloader.Try} objects. + * + * @param batchLoadFunction the batch load function to use that uses {@link org.dataloader.Try} objects + * @param options the options to use + * @param the key type + * @param the value type + * @return a new DataLoader + * @see #newDataLoaderWithTry(BatchLoader) + */ + public static DataLoader newDataLoaderWithTry(BatchLoaderWithContext> batchLoadFunction, DataLoaderOptions options) { + return new DataLoaderImpl<>(batchLoadFunction, options); + } + + /** + * Creates new DataLoader with the specified batch loader function and default options + * (batching, caching and unlimited batch size). + * + * @param batchLoadFunction the batch load function to use + * @param the key type + * @param the value type + * @return a new DataLoader + */ + public static DataLoader newMappedDataLoader(MappedBatchLoader batchLoadFunction) { + return newMappedDataLoader(batchLoadFunction, null); + } + + /** + * Creates new DataLoader with the specified batch loader function with the provided options + * + * @param batchLoadFunction the batch load function to use + * @param options the options to use + * @param the key type + * @param the value type + * @return a new DataLoader + */ + public static DataLoader newMappedDataLoader(MappedBatchLoader batchLoadFunction, DataLoaderOptions options) { + return new DataLoaderImpl<>(batchLoadFunction, options); + } + + /** + * Creates new DataLoader with the specified batch loader function and default options + * (batching, caching and unlimited batch size) where the batch loader function returns a list of + * {@link org.dataloader.Try} objects. + *

+ * If its important you to know the exact status of each item in a batch call and whether it threw exceptions then + * you can use this form to create the data loader. + *

+ * Using Try objects allows you to capture a value returned or an exception that might + * have occurred trying to get a value. . + *

+ * + * @param batchLoadFunction the batch load function to use that uses {@link org.dataloader.Try} objects + * @param the key type + * @param the value type + * @return a new DataLoader + */ + public static DataLoader newMappedDataLoaderWithTry(MappedBatchLoader> batchLoadFunction) { + return newMappedDataLoaderWithTry(batchLoadFunction, null); + } + + /** + * Creates new DataLoader with the specified batch loader function and with the provided options + * where the batch loader function returns a list of + * {@link org.dataloader.Try} objects. + * + * @param batchLoadFunction the batch load function to use that uses {@link org.dataloader.Try} objects + * @param options the options to use + * @param the key type + * @param the value type + * @return a new DataLoader + * @see #newDataLoaderWithTry(BatchLoader) + */ + public static DataLoader newMappedDataLoaderWithTry(MappedBatchLoader> batchLoadFunction, DataLoaderOptions options) { + return new DataLoaderImpl<>(batchLoadFunction, options); + } + + /** + * Creates new DataLoader with the specified mapped batch loader function and default options + * (batching, caching and unlimited batch size). + * + * @param batchLoadFunction the batch load function to use + * @param the key type + * @param the value type + * @return a new DataLoader + */ + public static DataLoader newMappedDataLoader(MappedBatchLoaderWithContext batchLoadFunction) { + return newMappedDataLoader(batchLoadFunction, null); + } + + /** + * Creates new DataLoader with the specified batch loader function with the provided options + * + * @param batchLoadFunction the batch load function to use + * @param options the options to use + * @param the key type + * @param the value type + * @return a new DataLoader + */ + public static DataLoader newMappedDataLoader(MappedBatchLoaderWithContext batchLoadFunction, DataLoaderOptions options) { + return new DataLoaderImpl<>(batchLoadFunction, options); + } + + /** + * Creates new DataLoader with the specified batch loader function and default options + * (batching, caching and unlimited batch size) where the batch loader function returns a list of + * {@link org.dataloader.Try} objects. + *

+ * If its important you to know the exact status of each item in a batch call and whether it threw exceptions then + * you can use this form to create the data loader. + *

+ * Using Try objects allows you to capture a value returned or an exception that might + * have occurred trying to get a value. . + * + * @param batchLoadFunction the batch load function to use that uses {@link org.dataloader.Try} objects + * @param the key type + * @param the value type + * @return a new DataLoader + */ + public static DataLoader newMappedDataLoaderWithTry(MappedBatchLoaderWithContext> batchLoadFunction) { + return newMappedDataLoaderWithTry(batchLoadFunction, null); + } + + /** + * Creates new DataLoader with the specified batch loader function and with the provided options + * where the batch loader function returns a list of + * {@link org.dataloader.Try} objects. + * + * @param batchLoadFunction the batch load function to use that uses {@link org.dataloader.Try} objects + * @param options the options to use + * @param the key type + * @param the value type + * @return a new DataLoader + * @see #newDataLoaderWithTry(BatchLoader) + */ + public static DataLoader newMappedDataLoaderWithTry(MappedBatchLoaderWithContext> batchLoadFunction, DataLoaderOptions options) { + return new DataLoaderImpl<>(batchLoadFunction, options); + } + +} diff --git a/src/main/java/org/dataloader/DataLoaderOptions.java b/src/main/java/org/dataloader/DataLoaderOptions.java index 8158902..d91f8d0 100644 --- a/src/main/java/org/dataloader/DataLoaderOptions.java +++ b/src/main/java/org/dataloader/DataLoaderOptions.java @@ -16,6 +16,7 @@ package org.dataloader; +import org.dataloader.annotations.PublicApi; import org.dataloader.stats.SimpleStatisticsCollector; import org.dataloader.stats.StatisticsCollector; @@ -29,6 +30,7 @@ * * @author Arnold Schrijver */ +@SuppressWarnings("rawtypes") @PublicApi public class DataLoaderOptions { @@ -38,7 +40,8 @@ public class DataLoaderOptions { private boolean cachingEnabled; private boolean cachingExceptionsEnabled; private CacheKey cacheKeyFunction; - private CacheMap cacheMap; + private CacheMap cacheMap; + private CacheMap promiseCacheMap; private int maxBatchSize; private Supplier statisticsCollector; private BatchLoaderContextProvider environmentProvider; @@ -67,6 +70,7 @@ public DataLoaderOptions(DataLoaderOptions other) { this.cachingExceptionsEnabled = other.cachingExceptionsEnabled; this.cacheKeyFunction = other.cacheKeyFunction; this.cacheMap = other.cacheMap; + this.promiseCacheMap = other.promiseCacheMap; this.maxBatchSize = other.maxBatchSize; this.statisticsCollector = other.statisticsCollector; this.environmentProvider = other.environmentProvider; @@ -92,7 +96,6 @@ public boolean batchingEnabled() { * Sets the option that determines whether batch loading is enabled. * * @param batchingEnabled {@code true} to enable batch loading, {@code false} otherwise - * * @return the data loader options for fluent coding */ public DataLoaderOptions setBatchingEnabled(boolean batchingEnabled) { @@ -113,7 +116,6 @@ public boolean cachingEnabled() { * Sets the option that determines whether caching is enabled. * * @param cachingEnabled {@code true} to enable caching, {@code false} otherwise - * * @return the data loader options for fluent coding */ public DataLoaderOptions setCachingEnabled(boolean cachingEnabled) { @@ -123,7 +125,7 @@ public DataLoaderOptions setCachingEnabled(boolean cachingEnabled) { /** * Option that determines whether to cache exceptional values (the default), or not. - * + *

* For short lived caches (that is request caches) it makes sense to cache exceptions since * its likely the key is still poisoned. However if you have long lived caches, then it may make * sense to set this to false since the downstream system may have recovered from its failure @@ -136,10 +138,9 @@ public boolean cachingExceptionsEnabled() { } /** - * Sets the option that determines whether exceptional values are cachedis enabled. + * Sets the option that determines whether exceptional values are cached. * * @param cachingExceptionsEnabled {@code true} to enable caching exceptional values, {@code false} otherwise - * * @return the data loader options for fluent coding */ public DataLoaderOptions setCachingExceptionsEnabled(boolean cachingExceptionsEnabled) { @@ -162,37 +163,69 @@ public Optional cacheKeyFunction() { * Sets the function to use for creating the cache key, if caching is enabled. * * @param cacheKeyFunction the cache key function to use - * * @return the data loader options for fluent coding */ - public DataLoaderOptions setCacheKeyFunction(CacheKey cacheKeyFunction) { + public DataLoaderOptions setCacheKeyFunction(CacheKey cacheKeyFunction) { this.cacheKeyFunction = cacheKeyFunction; return this; } /** - * Gets the (optional) cache map implementation that is used for caching, if caching is enabled. + * Gets the (optional) cache map implementation that is used for caching values, if caching is enabled. *

- * If missing a standard {@link java.util.LinkedHashMap} will be used as the cache implementation. + * If missing no value caching will be applied. + *

+ * The value cache is intended for a broader cache of values that can be serialised, perhaps a network + * distributed cache system like REDIS or MemCacheD. Contrast this to the {@link #promiseCacheMap} which holds JVM local + * {@link java.util.concurrent.CompletableFuture}s and hence cannot be serialised over the wire. * - * @return an optional with the cache map instance, or empty + * @return an optional with the value cache map instance, or empty */ - public Optional cacheMap() { + public Optional> cacheMap() { return Optional.ofNullable(cacheMap); } /** - * Sets the cache map implementation to use for caching, if caching is enabled. + * Gets the (optional) cache map implementation that is used for promise caching, if caching is enabled. + *

+ * If missing a standard {@link java.util.LinkedHashMap} will be used as the cache implementation. + *

+ * The promise cache is intended for a JVM local cache of {@link java.util.concurrent.CompletableFuture}s and hence cannot be serialised + * over the wire. Contrast this to the {@link #cacheMap()} which holds a broader values cache that could be serialised + * into a distributed cache system like REDIS or MemCacheD. * - * @param cacheMap the cache map instance + * @return an optional with the promise cache map instance, or empty + */ + public Optional> promiseCacheMap() { + return Optional.ofNullable(promiseCacheMap); + } + + /** + * Sets the cache map implementation to use for value caching, if caching is enabled. * + * @param cacheMap the cache map instance * @return the data loader options for fluent coding */ - public DataLoaderOptions setCacheMap(CacheMap cacheMap) { + public DataLoaderOptions setCacheMap(CacheMap cacheMap) { this.cacheMap = cacheMap; return this; } + /** + * Sets the cache map implementation to use for promise caching, if caching is enabled. + *

+ * Generally you are not expected to set this, as a JVM local default cache map + * will be used and is good enough for most situations however for completeness + * this method is offered. + * + * @param cacheMap the cache map instance + * @return the data loader options for fluent coding + */ + public DataLoaderOptions setPromiseCacheMap(CacheMap cacheMap) { + this.promiseCacheMap = cacheMap; + return this; + } + /** * Gets the maximum number of keys that will be presented to the {@link BatchLoader} function * before they are split into multiple class @@ -208,7 +241,6 @@ public int maxBatchSize() { * before they are split into multiple class * * @param maxBatchSize the maximum batch size - * * @return the data loader options for fluent coding */ public DataLoaderOptions setMaxBatchSize(int maxBatchSize) { @@ -229,7 +261,6 @@ public StatisticsCollector getStatisticsCollector() { * a common value * * @param statisticsCollector the statistics collector to use - * * @return the data loader options for fluent coding */ public DataLoaderOptions setStatisticsCollector(Supplier statisticsCollector) { @@ -248,7 +279,6 @@ public BatchLoaderContextProvider getBatchLoaderContextProvider() { * Sets the batch loader environment provider that will be used to give context to batch load functions * * @param contextProvider the batch loader context provider - * * @return the data loader options for fluent coding */ public DataLoaderOptions setBatchLoaderContextProvider(BatchLoaderContextProvider contextProvider) { diff --git a/src/main/java/org/dataloader/DataLoaderRegistry.java b/src/main/java/org/dataloader/DataLoaderRegistry.java index bf9b2c6..ad1a8a0 100644 --- a/src/main/java/org/dataloader/DataLoaderRegistry.java +++ b/src/main/java/org/dataloader/DataLoaderRegistry.java @@ -1,5 +1,8 @@ package org.dataloader; +import org.dataloader.annotations.PublicApi; +import org.dataloader.stats.Statistics; + import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -8,8 +11,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; -import org.dataloader.stats.Statistics; - /** * This allows data loaders to be registered together into a single place so * they can be dispatched as one. It also allows you to retrieve data loaders by @@ -104,23 +105,15 @@ public Set getKeys() { } /** - * This will called {@link org.dataloader.DataLoader#dispatch()} on each of the registered - * {@link org.dataloader.DataLoader}s - */ - public void dispatchAll() { - getDataLoaders().forEach(DataLoader::dispatch); - } - - /** - * Similar to {@link DataLoaderRegistry#dispatchAll()}, this calls {@link org.dataloader.DataLoader#dispatch()} on - * each of the registered {@link org.dataloader.DataLoader}s, but returns the number of dispatches. + * SThis calls {@link org.dataloader.DataLoader#dispatch()} on + * each of the registered {@link org.dataloader.DataLoader}s, and returns the number of dispatches. * * @return total number of entries that were dispatched from registered {@link org.dataloader.DataLoader}s. */ - public int dispatchAllWithCount() { + public int dispatchAll() { int sum = 0; - for (DataLoader dataLoader : getDataLoaders()) { - sum += dataLoader.dispatchWithCounts().getKeysCount(); + for (DataLoader dataLoader : getDataLoaders()) { + sum += dataLoader.dispatch().getKeysCount(); } return sum; } @@ -131,7 +124,7 @@ public int dispatchAllWithCount() { */ public int dispatchDepth() { int totalDispatchDepth = 0; - for (DataLoader dataLoader : getDataLoaders()) { + for (DataLoader dataLoader : getDataLoaders()) { totalDispatchDepth += dataLoader.dispatchDepth(); } return totalDispatchDepth; diff --git a/src/main/java/org/dataloader/DispatchResult.java b/src/main/java/org/dataloader/DispatchResult.java index c1b41aa..97711da 100644 --- a/src/main/java/org/dataloader/DispatchResult.java +++ b/src/main/java/org/dataloader/DispatchResult.java @@ -1,5 +1,7 @@ package org.dataloader; +import org.dataloader.annotations.PublicApi; + import java.util.List; import java.util.concurrent.CompletableFuture; diff --git a/src/main/java/org/dataloader/Try.java b/src/main/java/org/dataloader/Try.java index 6a7f44e..e273155 100644 --- a/src/main/java/org/dataloader/Try.java +++ b/src/main/java/org/dataloader/Try.java @@ -1,5 +1,7 @@ package org.dataloader; +import org.dataloader.annotations.PublicApi; + import java.util.Optional; import java.util.concurrent.Callable; import java.util.concurrent.CompletionStage; diff --git a/src/main/java/org/dataloader/Internal.java b/src/main/java/org/dataloader/annotations/Internal.java similarity index 95% rename from src/main/java/org/dataloader/Internal.java rename to src/main/java/org/dataloader/annotations/Internal.java index 736c033..4ad04cd 100644 --- a/src/main/java/org/dataloader/Internal.java +++ b/src/main/java/org/dataloader/annotations/Internal.java @@ -1,4 +1,4 @@ -package org.dataloader; +package org.dataloader.annotations; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; diff --git a/src/main/java/org/dataloader/PublicApi.java b/src/main/java/org/dataloader/annotations/PublicApi.java similarity index 95% rename from src/main/java/org/dataloader/PublicApi.java rename to src/main/java/org/dataloader/annotations/PublicApi.java index d2472e9..157c0b1 100644 --- a/src/main/java/org/dataloader/PublicApi.java +++ b/src/main/java/org/dataloader/annotations/PublicApi.java @@ -1,4 +1,4 @@ -package org.dataloader; +package org.dataloader.annotations; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/src/main/java/org/dataloader/PublicSpi.java b/src/main/java/org/dataloader/annotations/PublicSpi.java similarity index 96% rename from src/main/java/org/dataloader/PublicSpi.java rename to src/main/java/org/dataloader/annotations/PublicSpi.java index 86a43e9..5f385b7 100644 --- a/src/main/java/org/dataloader/PublicSpi.java +++ b/src/main/java/org/dataloader/annotations/PublicSpi.java @@ -1,4 +1,4 @@ -package org.dataloader; +package org.dataloader.annotations; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/src/main/java/org/dataloader/impl/Assertions.java b/src/main/java/org/dataloader/impl/Assertions.java index 3b09814..627481e 100644 --- a/src/main/java/org/dataloader/impl/Assertions.java +++ b/src/main/java/org/dataloader/impl/Assertions.java @@ -1,6 +1,21 @@ +/* + * 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 org.dataloader.impl; -import org.dataloader.Internal; +import org.dataloader.annotations.Internal; import java.util.Objects; diff --git a/src/main/java/org/dataloader/impl/CompletableFutureKit.java b/src/main/java/org/dataloader/impl/CompletableFutureKit.java index d4b9f79..6e21635 100644 --- a/src/main/java/org/dataloader/impl/CompletableFutureKit.java +++ b/src/main/java/org/dataloader/impl/CompletableFutureKit.java @@ -1,6 +1,6 @@ package org.dataloader.impl; -import org.dataloader.Internal; +import org.dataloader.annotations.Internal; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -20,7 +20,7 @@ public static CompletableFuture failedFuture(Exception e) { return future; } - public static Throwable cause(CompletableFuture completableFuture) { + public static Throwable cause(CompletableFuture completableFuture) { if (!completableFuture.isCompletedExceptionally()) { return null; } @@ -38,11 +38,11 @@ public static Throwable cause(CompletableFuture completableFuture) { } } - public static boolean succeeded(CompletableFuture future) { + public static boolean succeeded(CompletableFuture future) { return future.isDone() && !future.isCompletedExceptionally(); } - public static boolean failed(CompletableFuture future) { + public static boolean failed(CompletableFuture future) { return future.isDone() && future.isCompletedExceptionally(); } diff --git a/src/main/java/org/dataloader/DataLoaderHelper.java b/src/main/java/org/dataloader/impl/DataLoaderImpl.java similarity index 60% rename from src/main/java/org/dataloader/DataLoaderHelper.java rename to src/main/java/org/dataloader/impl/DataLoaderImpl.java index f2437be..e15ba9c 100644 --- a/src/main/java/org/dataloader/DataLoaderHelper.java +++ b/src/main/java/org/dataloader/impl/DataLoaderImpl.java @@ -1,10 +1,37 @@ -package org.dataloader; - -import org.dataloader.impl.CompletableFutureKit; +/* + * 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 org.dataloader.impl; + +import org.dataloader.BatchLoader; +import org.dataloader.BatchLoaderEnvironment; +import org.dataloader.BatchLoaderWithContext; +import org.dataloader.CacheMap; +import org.dataloader.DataLoader; +import org.dataloader.DataLoaderOptions; +import org.dataloader.DispatchResult; +import org.dataloader.MappedBatchLoader; +import org.dataloader.MappedBatchLoaderWithContext; +import org.dataloader.Try; +import org.dataloader.annotations.Internal; +import org.dataloader.stats.Statistics; import org.dataloader.stats.StatisticsCollector; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -19,63 +46,173 @@ import static org.dataloader.impl.Assertions.assertState; import static org.dataloader.impl.Assertions.nonNull; -/** - * This helps break up the large DataLoader class functionality and it contains the logic to dispatch the - * promises on behalf of its peer dataloader - * - * @param the type of keys - * @param the type of values - */ @Internal -class DataLoaderHelper { +public class DataLoaderImpl implements DataLoader { + private final Object batchLoadFunction; + private final DataLoaderOptions loaderOptions; + private final CacheMap> futureCache; + private final CacheMap valueCache; + private final List>> loaderQueue; + private final StatisticsCollector stats; - class LoaderQueueEntry { + @Internal + public DataLoaderImpl(Object batchLoadFunction, DataLoaderOptions options) { + DataLoaderOptions loaderOptions = options == null ? new DataLoaderOptions() : options; + // order of keys matter in data loader + this.stats = nonNull(loaderOptions.getStatisticsCollector()); + this.batchLoadFunction = batchLoadFunction; + this.loaderOptions = loaderOptions; + this.futureCache = determinePromiseCacheMap(loaderOptions); + this.valueCache = determineValueCacheMap(loaderOptions); + this.loaderQueue = new ArrayList<>(); + } - final K key; - final V value; - final Object callContext; + @Override + public CompletableFuture load(K key) { + return load(key, null); + } - public LoaderQueueEntry(K key, V value, Object callContext) { - this.key = key; - this.value = value; - this.callContext = callContext; + @Override + public Optional> getIfPresent(K key) { + return getIfPresentImpl(key); + } + + @Override + public Optional> getIfCompleted(K key) { + return getIfCompletedImpl(key); + } + + @Override + public CompletableFuture load(K key, Object keyContext) { + return loadImpl(key, keyContext); + } + + @Override + public CompletableFuture> loadMany(List keys) { + return loadMany(keys, Collections.emptyList()); + } + + @Override + public CompletableFuture> loadMany(List keys, List keyContexts) { + nonNull(keys); + nonNull(keyContexts); + + synchronized (this) { + List> collect = new ArrayList<>(); + for (int i = 0; i < keys.size(); i++) { + K key = keys.get(i); + Object keyContext = null; + if (i < keyContexts.size()) { + keyContext = keyContexts.get(i); + } + collect.add(load(key, keyContext)); + } + return CompletableFutureKit.allOf(collect); } + } + + @Override + public DispatchResult dispatch() { + return dispatchImpl(); + } - K getKey() { - return key; + @Override + public List dispatchAndJoin() { + + List joinedResults = dispatch().getPromisedResults().join(); + List results = new ArrayList<>(joinedResults); + while (this.dispatchDepth() > 0) { + joinedResults = dispatch().getPromisedResults().join(); + results.addAll(joinedResults); } + return results; + } - V getValue() { - return value; + @Override + public int dispatchDepth() { + synchronized (this) { + return loaderQueue.size(); } + } - Object getCallContext() { - return callContext; + public DataLoader clear(K key) { + Object cacheKey = getCacheKey(key); + synchronized (this) { + futureCache.delete(cacheKey); + if (valueCache != null) { + valueCache.delete(cacheKey); + } } + return this; } - private final DataLoader dataLoader; - private final Object batchLoadFunction; - private final DataLoaderOptions loaderOptions; - private final CacheMap> futureCache; - private final List>> loaderQueue; - private final StatisticsCollector stats; + public DataLoader clearAll() { + synchronized (this) { + futureCache.clear(); + if (valueCache != null) { + valueCache.clear(); + } + } + return this; + } - DataLoaderHelper(DataLoader dataLoader, Object batchLoadFunction, DataLoaderOptions loaderOptions, CacheMap> futureCache, StatisticsCollector stats) { - this.dataLoader = dataLoader; - this.batchLoadFunction = batchLoadFunction; - this.loaderOptions = loaderOptions; - this.futureCache = futureCache; - this.loaderQueue = new ArrayList<>(); - this.stats = stats; + public DataLoader prime(K key, V value) { + Object cacheKey = getCacheKey(key); + synchronized (this) { + if (!futureCache.containsKey(cacheKey)) { + futureCache.set(cacheKey, CompletableFuture.completedFuture(value)); + } + if (valueCache != null) { + if (!valueCache.containsKey(cacheKey)) { + valueCache.set(cacheKey, value); + } + } + } + return this; } - Optional> getIfPresent(K key) { - synchronized (dataLoader) { - Object cacheKey = getCacheKey(nonNull(key)); - boolean cachingEnabled = loaderOptions.cachingEnabled(); - if (cachingEnabled) { + public DataLoader prime(K key, Exception error) { + Object cacheKey = getCacheKey(key); + synchronized (this) { + if (!futureCache.containsKey(cacheKey)) { + futureCache.set(cacheKey, CompletableFutureKit.failedFuture(error)); + } + if (valueCache != null) { + valueCache.delete(cacheKey); + } + } + return this; + } + + @SuppressWarnings("unchecked") + public Object getCacheKey(K key) { + return loaderOptions.cacheKeyFunction() + .map(cacheKeyFunction -> cacheKeyFunction.getKey(key)) + .orElse(key); + } + + @Override + public Statistics getStatistics() { + return stats.getStatistics(); + } + + @SuppressWarnings("unchecked") + private CacheMap> determinePromiseCacheMap(DataLoaderOptions loaderOptions) { + CacheMap promiseMap = loaderOptions.promiseCacheMap().orElse(CacheMap.simpleMap()); + return (CacheMap>) promiseMap; + } + + @SuppressWarnings("unchecked") + private CacheMap determineValueCacheMap(DataLoaderOptions loaderOptions) { + return loaderOptions.cacheMap().isPresent() ? (CacheMap) loaderOptions.cacheMap().get() : null; + } + + Optional> getIfPresentImpl(K key) { + Object cacheKey = getCacheKey(nonNull(key)); + boolean cachingEnabled = loaderOptions.cachingEnabled(); + if (cachingEnabled) { + synchronized (this) { if (futureCache.containsKey(cacheKey)) { stats.incrementCacheHitCount(); return Optional.of(futureCache.get(cacheKey)); @@ -85,33 +222,32 @@ Optional> getIfPresent(K key) { return Optional.empty(); } - Optional> getIfCompleted(K key) { - synchronized (dataLoader) { - Optional> cachedPromise = getIfPresent(key); - if (cachedPromise.isPresent()) { - CompletableFuture promise = cachedPromise.get(); - if (promise.isDone()) { - return cachedPromise; - } - } - } - return Optional.empty(); + Optional> getIfCompletedImpl(K key) { + return getIfPresent(key).filter(CompletableFuture::isDone); } + CompletableFuture loadImpl(K key, Object loadContext) { + Object cacheKey = getCacheKey(nonNull(key)); + boolean batchingEnabled = loaderOptions.batchingEnabled(); + boolean cachingEnabled = loaderOptions.cachingEnabled(); - CompletableFuture load(K key, Object loadContext) { - synchronized (dataLoader) { - Object cacheKey = getCacheKey(nonNull(key)); + synchronized (this) { stats.incrementLoadCount(); - boolean batchingEnabled = loaderOptions.batchingEnabled(); - boolean cachingEnabled = loaderOptions.cachingEnabled(); - if (cachingEnabled) { if (futureCache.containsKey(cacheKey)) { stats.incrementCacheHitCount(); return futureCache.get(cacheKey); } + if (valueCache != null) { + V value = valueCache.get(cacheKey); + if (value != null) { + stats.incrementCacheHitCount(); + CompletableFuture completedValue = CompletableFuture.completedFuture(value); + futureCache.set(cacheKey, completedValue); + return completedValue; + } + } } CompletableFuture future = new CompletableFuture<>(); @@ -124,34 +260,32 @@ CompletableFuture load(K key, Object loadContext) { } if (cachingEnabled) { futureCache.set(cacheKey, future); + if (valueCache != null) { + // when the promise finally finishes back update the value cache + future.thenAccept(value -> valueCache.set(cacheKey, value)); + } } return future; } } - @SuppressWarnings("unchecked") - Object getCacheKey(K key) { - return loaderOptions.cacheKeyFunction().isPresent() ? - loaderOptions.cacheKeyFunction().get().getKey(key) : key; - } - - DispatchResult dispatch() { + DispatchResult dispatchImpl() { boolean batchingEnabled = loaderOptions.batchingEnabled(); // // we copy the pre-loaded set of futures ready for dispatch final List keys = new ArrayList<>(); final List callContexts = new ArrayList<>(); final List> queuedFutures = new ArrayList<>(); - synchronized (dataLoader) { + synchronized (this) { loaderQueue.forEach(entry -> { - keys.add(entry.getKey()); - queuedFutures.add(entry.getValue()); - callContexts.add(entry.getCallContext()); + keys.add(entry.key); + queuedFutures.add(entry.value); + callContexts.add(entry.callContext); }); loaderQueue.clear(); } if (!batchingEnabled || keys.isEmpty()) { - return new DispatchResult(CompletableFuture.completedFuture(emptyList()), 0); + return new DispatchResult<>(CompletableFuture.completedFuture(emptyList()), 0); } final int totalEntriesHandled = keys.size(); // @@ -172,7 +306,7 @@ DispatchResult dispatch() { } else { futureList = dispatchQueueBatch(keys, callContexts, queuedFutures); } - return new DispatchResult(futureList, totalEntriesHandled); + return new DispatchResult<>(futureList, totalEntriesHandled); } private CompletableFuture> sliceIntoBatchesOfBatches(List keys, List> queuedFutures, List callContexts, int maxBatchSize) { @@ -194,7 +328,7 @@ private CompletableFuture> sliceIntoBatchesOfBatches(List keys, List< } // // now reassemble all the futures into one that is the complete set of results - return CompletableFuture.allOf(allBatches.toArray(new CompletableFuture[allBatches.size()])) + return CompletableFuture.allOf(allBatches.toArray(new CompletableFuture[0])) .thenApply(v -> allBatches.stream() .map(CompletableFuture::join) .flatMap(Collection::stream) @@ -212,7 +346,7 @@ private CompletableFuture> dispatchQueueBatch(List keys, List List clearCacheKeys = new ArrayList<>(); for (int idx = 0; idx < queuedFutures.size(); idx++) { - Object value = values.get(idx); + V value = values.get(idx); CompletableFuture future = queuedFutures.get(idx); if (value instanceof Throwable) { stats.incrementLoadErrorCount(); @@ -230,8 +364,7 @@ private CompletableFuture> dispatchQueueBatch(List keys, List clearCacheKeys.add(keys.get(idx)); } } else { - V val = (V) value; - future.complete(val); + future.complete(value); } } possiblyClearCacheEntriesOnExceptions(clearCacheKeys); @@ -243,13 +376,12 @@ private CompletableFuture> dispatchQueueBatch(List keys, List CompletableFuture future = queuedFutures.get(idx); future.completeExceptionally(ex); // clear any cached view of this key because they all failed - dataLoader.clear(key); + clear(key); } return emptyList(); }); } - private void assertResultSize(List keys, List values) { assertState(keys.size() == values.size(), "The size of the promised values MUST be the same size as the key list"); } @@ -263,11 +395,10 @@ private void possiblyClearCacheEntriesOnExceptions(List keys) { // but might work against long lived caches. Hence we have an option that allows // it to be cleared if (!loaderOptions.cachingExceptionsEnabled()) { - keys.forEach(dataLoader::clear); + keys.forEach(this::clear); } } - CompletableFuture invokeLoaderImmediately(K key, Object keyContext) { List keys = singletonList(key); CompletionStage singleLoadCall; @@ -314,7 +445,6 @@ private CompletionStage> invokeListBatchLoader(List keys, BatchLoader return nonNull(loadResult, "Your batch loader function MUST return a non null CompletionStage promise"); } - /* * Turns a map of results that MAY be smaller than the key list back into a list by mapping null * to missing elements. @@ -343,9 +473,16 @@ private boolean isMapLoader() { return batchLoadFunction instanceof MappedBatchLoader || batchLoadFunction instanceof MappedBatchLoaderWithContext; } - int dispatchDepth() { - synchronized (dataLoader) { - return loaderQueue.size(); + static class LoaderQueueEntry { + + final K key; + final V value; + final Object callContext; + + LoaderQueueEntry(K key, V value, Object callContext) { + this.key = key; + this.value = value; + this.callContext = callContext; } } } diff --git a/src/main/java/org/dataloader/impl/DefaultCacheMap.java b/src/main/java/org/dataloader/impl/DefaultCacheMap.java index 4dcddab..b8b4a2d 100644 --- a/src/main/java/org/dataloader/impl/DefaultCacheMap.java +++ b/src/main/java/org/dataloader/impl/DefaultCacheMap.java @@ -17,7 +17,7 @@ package org.dataloader.impl; import org.dataloader.CacheMap; -import org.dataloader.Internal; +import org.dataloader.annotations.Internal; import java.util.HashMap; import java.util.Map; diff --git a/src/main/java/org/dataloader/impl/PromisedValues.java b/src/main/java/org/dataloader/impl/PromisedValues.java index 0f992f8..2ce2f37 100644 --- a/src/main/java/org/dataloader/impl/PromisedValues.java +++ b/src/main/java/org/dataloader/impl/PromisedValues.java @@ -1,6 +1,6 @@ package org.dataloader.impl; -import org.dataloader.Internal; +import org.dataloader.annotations.Internal; import java.util.List; import java.util.concurrent.CancellationException; @@ -35,7 +35,6 @@ public interface PromisedValues { * * @param cfs the {@link CompletionStage}s to combine * @param the type of values - * * @return a new PromisedValues */ static PromisedValues allOf(List> cfs) { @@ -51,7 +50,6 @@ static PromisedValues allOf(List> cfs) { * @param f1 the 1st completable future * @param f2 the 2nd completable future * @param the type of values - * * @return a new PromisedValues */ static PromisedValues allOf(CompletionStage f1, CompletionStage f2) { @@ -68,7 +66,6 @@ static PromisedValues allOf(CompletionStage f1, CompletionStage f2) * @param f2 the 2nd completable future * @param f3 the 3rd completable future * @param the type of values - * * @return a new PromisedValues */ static PromisedValues allOf(CompletionStage f1, CompletionStage f2, CompletionStage f3) { @@ -87,7 +84,6 @@ static PromisedValues allOf(CompletionStage f1, CompletionStage f2, * @param f3 the 3rd completable future * @param f4 the 4th completable future * @param the type of values - * * @return a new PromisedValues */ static PromisedValues allOf(CompletionStage f1, CompletionStage f2, CompletionStage f3, CompletionStage f4) { @@ -103,7 +99,6 @@ static PromisedValues allOf(CompletionStage f1, CompletionStage f2, * * @param cfs the list to combine * @param the type of values - * * @return a new PromisedValues */ static PromisedValues allPromisedValues(List> cfs) { @@ -119,7 +114,6 @@ static PromisedValues allPromisedValues(List> cfs) { * @param pv1 the 1st promised value * @param pv2 the 2nd promised value * @param the type of values - * * @return a new PromisedValues */ static PromisedValues allPromisedValues(PromisedValues pv1, PromisedValues pv2) { @@ -136,7 +130,6 @@ static PromisedValues allPromisedValues(PromisedValues pv1, PromisedVa * @param pv2 the 2nd promised value * @param pv3 the 3rd promised value * @param the type of values - * * @return a new PromisedValues */ static PromisedValues allPromisedValues(PromisedValues pv1, PromisedValues pv2, PromisedValues pv3) { @@ -154,7 +147,6 @@ static PromisedValues allPromisedValues(PromisedValues pv1, PromisedVa * @param pv3 the 3rd promised value * @param pv4 the 4th promised value * @param the type of values - * * @return a new PromisedValues */ static PromisedValues allPromisedValues(PromisedValues pv1, PromisedValues pv2, PromisedValues pv3, PromisedValues pv4) { @@ -166,7 +158,6 @@ static PromisedValues allPromisedValues(PromisedValues pv1, PromisedVa * When the all the futures complete, this call back will be invoked with this {@link PromisedValues} as a parameter * * @param handler the call back which will be given this object - * * @return a new {@link PromisedValues} which you can compose more computations with */ PromisedValues thenAccept(Consumer> handler); @@ -199,7 +190,6 @@ static PromisedValues allPromisedValues(PromisedValues pv1, PromisedVa * The true if the {@link CompletionStage} at the specified index succeeded * * @param index the index of the {@link CompletionStage} - * * @return true if the future at the specified index succeeded */ boolean succeeded(int index); @@ -208,7 +198,6 @@ static PromisedValues allPromisedValues(PromisedValues pv1, PromisedVa * The exception cause at the specified index or null if it didn't fail * * @param index the index of the {@link CompletionStage} - * * @return an exception or null if the future did not fail */ Throwable cause(int index); @@ -217,10 +206,8 @@ static PromisedValues allPromisedValues(PromisedValues pv1, PromisedVa * The value at index or null if it failed * * @param index the index of the future - * * @return the value of the future */ - @SuppressWarnings("unchecked") T get(int index); /** @@ -244,7 +231,6 @@ static PromisedValues allPromisedValues(PromisedValues pv1, PromisedVa * exception as its cause. * * @return the list of completed values similar to {@link #toList()} - * * @throws CancellationException if the computation was cancelled * @throws CompletionException if this future completed * exceptionally or a completion computation threw an exception diff --git a/src/main/java/org/dataloader/impl/PromisedValuesImpl.java b/src/main/java/org/dataloader/impl/PromisedValuesImpl.java index 6c7ee49..bca9d52 100644 --- a/src/main/java/org/dataloader/impl/PromisedValuesImpl.java +++ b/src/main/java/org/dataloader/impl/PromisedValuesImpl.java @@ -1,6 +1,6 @@ package org.dataloader.impl; -import org.dataloader.Internal; +import org.dataloader.annotations.Internal; import java.util.ArrayList; import java.util.List; @@ -25,8 +25,7 @@ public class PromisedValuesImpl implements PromisedValues { private PromisedValuesImpl(List> cs) { this.futures = nonNull(cs); this.cause = new AtomicReference<>(); - List cfs = cs.stream().map(CompletionStage::toCompletableFuture).collect(Collectors.toList()); - CompletableFuture[] futuresArray = cfs.toArray(new CompletableFuture[cfs.size()]); + CompletableFuture[] futuresArray = cs.stream().map(CompletionStage::toCompletableFuture).toArray(CompletableFuture[]::new); this.controller = CompletableFuture.allOf(futuresArray).handle((result, throwable) -> { setCause(throwable); return null; @@ -104,7 +103,6 @@ public Throwable cause(int index) { } @Override - @SuppressWarnings("unchecked") public T get(int index) { assertState(isDone(), "The PromisedValues MUST be complete before calling the get() method"); try { diff --git a/src/main/java/org/dataloader/stats/Statistics.java b/src/main/java/org/dataloader/stats/Statistics.java index 6e0d102..4bc9c69 100644 --- a/src/main/java/org/dataloader/stats/Statistics.java +++ b/src/main/java/org/dataloader/stats/Statistics.java @@ -1,6 +1,6 @@ package org.dataloader.stats; -import org.dataloader.PublicApi; +import org.dataloader.annotations.PublicApi; import java.util.LinkedHashMap; import java.util.Map; diff --git a/src/main/java/org/dataloader/stats/StatisticsCollector.java b/src/main/java/org/dataloader/stats/StatisticsCollector.java index 49c979f..8fde3e4 100644 --- a/src/main/java/org/dataloader/stats/StatisticsCollector.java +++ b/src/main/java/org/dataloader/stats/StatisticsCollector.java @@ -1,6 +1,6 @@ package org.dataloader.stats; -import org.dataloader.PublicSpi; +import org.dataloader.annotations.PublicSpi; /** * This allows statistics to be collected for {@link org.dataloader.DataLoader} operations diff --git a/src/test/java/ReadmeExamples.java b/src/test/java/ReadmeExamples.java index 523cb5a..ccdd555 100644 --- a/src/test/java/ReadmeExamples.java +++ b/src/test/java/ReadmeExamples.java @@ -3,6 +3,7 @@ import org.dataloader.BatchLoaderWithContext; import org.dataloader.CacheMap; import org.dataloader.DataLoader; +import org.dataloader.DataLoaderFactory; import org.dataloader.DataLoaderOptions; import org.dataloader.MappedBatchLoaderWithContext; import org.dataloader.Try; @@ -59,7 +60,7 @@ public CompletionStage> load(List userIds) { } }; - DataLoader userLoader = DataLoader.newDataLoader(userBatchLoader); + DataLoader userLoader = DataLoaderFactory.newDataLoader(userBatchLoader); CompletionStage load1 = userLoader.load(1L); @@ -96,7 +97,7 @@ public CompletionStage> load(List keys, BatchLoaderEnvironm } }; - DataLoader loader = DataLoader.newDataLoader(batchLoader, options); + DataLoader loader = DataLoaderFactory.newDataLoader(batchLoader, options); } private void keyContextExample() { @@ -120,7 +121,7 @@ public CompletionStage> load(List keys, BatchLoaderEnvironm } }; - DataLoader loader = DataLoader.newDataLoader(batchLoader, options); + DataLoader loader = DataLoaderFactory.newDataLoader(batchLoader, options); loader.load("keyA", "contextForA"); loader.load("keyB", "contextForB"); } @@ -138,7 +139,7 @@ public CompletionStage> load(Set userIds, BatchLoaderEnvir } }; - DataLoader userLoader = DataLoader.newMappedDataLoader(mapBatchLoader); + DataLoader userLoader = DataLoaderFactory.newMappedDataLoader(mapBatchLoader); // ... } @@ -162,7 +163,7 @@ private void tryExample() { } private void tryBatcLoader() { - DataLoader dataLoader = DataLoader.newDataLoaderWithTry(new BatchLoader>() { + DataLoader dataLoader = DataLoaderFactory.newDataLoaderWithTry(new BatchLoader>() { @Override public CompletionStage>> load(List keys) { return CompletableFuture.supplyAsync(() -> { @@ -194,7 +195,7 @@ private void clearCacheOnError() { BatchLoader userBatchLoader; private void disableCache() { - DataLoader.newDataLoader(userBatchLoader, DataLoaderOptions.newOptions().setCachingEnabled(false)); + DataLoaderFactory.newDataLoader(userBatchLoader, DataLoaderOptions.newOptions().setCachingEnabled(false)); userDataLoader.load("A"); @@ -237,7 +238,7 @@ private void customCache() { MyCustomCache customCache = new MyCustomCache(); DataLoaderOptions options = DataLoaderOptions.newOptions().setCacheMap(customCache); - DataLoader.newDataLoader(userBatchLoader, options); + DataLoaderFactory.newDataLoader(userBatchLoader, options); } private void processUser(User user) { @@ -265,7 +266,7 @@ private void statsExample() { private void statsConfigExample() { DataLoaderOptions options = DataLoaderOptions.newOptions().setStatisticsCollector(() -> new ThreadLocalStatisticsCollector()); - DataLoader userDataLoader = DataLoader.newDataLoader(userBatchLoader, options); + DataLoader userDataLoader = DataLoaderFactory.newDataLoader(userBatchLoader, options); } } diff --git a/src/test/java/org/dataloader/DataLoaderBatchLoaderEnvironmentTest.java b/src/test/java/org/dataloader/DataLoaderBatchLoaderEnvironmentTest.java index 575fffd..0e7caca 100644 --- a/src/test/java/org/dataloader/DataLoaderBatchLoaderEnvironmentTest.java +++ b/src/test/java/org/dataloader/DataLoaderBatchLoaderEnvironmentTest.java @@ -36,14 +36,14 @@ private BatchLoaderWithContext contextBatchLoader() { @Test - public void context_is_passed_to_batch_loader_function() throws Exception { + public void context_is_passed_to_batch_loader_function() { BatchLoaderWithContext batchLoader = (keys, environment) -> { List list = keys.stream().map(k -> k + "-" + environment.getContext()).collect(Collectors.toList()); return CompletableFuture.completedFuture(list); }; DataLoaderOptions options = DataLoaderOptions.newOptions() .setBatchLoaderContextProvider(() -> "ctx"); - DataLoader loader = DataLoader.newDataLoader(batchLoader, options); + DataLoader loader = DataLoaderFactory.newDataLoader(batchLoader, options); loader.load("A"); loader.load("B"); @@ -55,11 +55,11 @@ public void context_is_passed_to_batch_loader_function() throws Exception { } @Test - public void key_contexts_are_passed_to_batch_loader_function() throws Exception { + public void key_contexts_are_passed_to_batch_loader_function() { BatchLoaderWithContext batchLoader = contextBatchLoader(); DataLoaderOptions options = DataLoaderOptions.newOptions() .setBatchLoaderContextProvider(() -> "ctx"); - DataLoader loader = DataLoader.newDataLoader(batchLoader, options); + DataLoader loader = DataLoaderFactory.newDataLoader(batchLoader, options); loader.load("A", "aCtx"); loader.load("B", "bCtx"); @@ -71,12 +71,12 @@ public void key_contexts_are_passed_to_batch_loader_function() throws Exception } @Test - public void key_contexts_are_passed_to_batch_loader_function_when_batching_disabled() throws Exception { + public void key_contexts_are_passed_to_batch_loader_function_when_batching_disabled() { BatchLoaderWithContext batchLoader = contextBatchLoader(); DataLoaderOptions options = DataLoaderOptions.newOptions() .setBatchingEnabled(false) .setBatchLoaderContextProvider(() -> "ctx"); - DataLoader loader = DataLoader.newDataLoader(batchLoader, options); + DataLoader loader = DataLoaderFactory.newDataLoader(batchLoader, options); CompletableFuture aLoad = loader.load("A", "aCtx"); CompletableFuture bLoad = loader.load("B", "bCtx"); @@ -89,11 +89,11 @@ public void key_contexts_are_passed_to_batch_loader_function_when_batching_disab } @Test - public void missing_key_contexts_are_passed_to_batch_loader_function() throws Exception { + public void missing_key_contexts_are_passed_to_batch_loader_function() { BatchLoaderWithContext batchLoader = contextBatchLoader(); DataLoaderOptions options = DataLoaderOptions.newOptions() .setBatchLoaderContextProvider(() -> "ctx"); - DataLoader loader = DataLoader.newDataLoader(batchLoader, options); + DataLoader loader = DataLoaderFactory.newDataLoader(batchLoader, options); loader.load("A", "aCtx"); loader.load("B"); @@ -105,7 +105,7 @@ public void missing_key_contexts_are_passed_to_batch_loader_function() throws Ex } @Test - public void context_is_passed_to_map_batch_loader_function() throws Exception { + public void context_is_passed_to_map_batch_loader_function() { MappedBatchLoaderWithContext mapBatchLoader = (keys, environment) -> { Map map = new HashMap<>(); keys.forEach(k -> { @@ -117,7 +117,7 @@ public void context_is_passed_to_map_batch_loader_function() throws Exception { }; DataLoaderOptions options = DataLoaderOptions.newOptions() .setBatchLoaderContextProvider(() -> "ctx"); - DataLoader loader = DataLoader.newMappedDataLoader(mapBatchLoader, options); + DataLoader loader = DataLoaderFactory.newMappedDataLoader(mapBatchLoader, options); loader.load("A", "aCtx"); loader.load("B"); @@ -129,12 +129,12 @@ public void context_is_passed_to_map_batch_loader_function() throws Exception { } @Test - public void null_is_passed_as_context_if_you_do_nothing() throws Exception { + public void null_is_passed_as_context_if_you_do_nothing() { BatchLoaderWithContext batchLoader = (keys, environment) -> { List list = keys.stream().map(k -> k + "-" + environment.getContext()).collect(Collectors.toList()); return CompletableFuture.completedFuture(list); }; - DataLoader loader = DataLoader.newDataLoader(batchLoader); + DataLoader loader = DataLoaderFactory.newDataLoader(batchLoader); loader.load("A"); loader.load("B"); @@ -146,13 +146,13 @@ public void null_is_passed_as_context_if_you_do_nothing() throws Exception { } @Test - public void null_is_passed_as_context_to_map_loader_if_you_do_nothing() throws Exception { + public void null_is_passed_as_context_to_map_loader_if_you_do_nothing() { MappedBatchLoaderWithContext mapBatchLoader = (keys, environment) -> { Map map = new HashMap<>(); keys.forEach(k -> map.put(k, k + "-" + environment.getContext())); return CompletableFuture.completedFuture(map); }; - DataLoader loader = DataLoader.newMappedDataLoader(mapBatchLoader); + DataLoader loader = DataLoaderFactory.newMappedDataLoader(mapBatchLoader); loader.load("A"); loader.load("B"); @@ -164,12 +164,12 @@ public void null_is_passed_as_context_to_map_loader_if_you_do_nothing() throws E } @Test - public void mmap_semantics_apply_to_batch_loader_context() throws Exception { + public void mmap_semantics_apply_to_batch_loader_context() { BatchLoaderWithContext batchLoader = contextBatchLoader(); DataLoaderOptions options = DataLoaderOptions.newOptions() .setBatchLoaderContextProvider(() -> "ctx") .setCachingEnabled(false); - DataLoader loader = DataLoader.newDataLoader(batchLoader, options); + DataLoader loader = DataLoaderFactory.newDataLoader(batchLoader, options); loader.load("A", "aCtx"); loader.load("B", "bCtx"); diff --git a/src/test/java/org/dataloader/DataLoaderCacheTest.java b/src/test/java/org/dataloader/DataLoaderCacheTest.java new file mode 100644 index 0000000..3c3da7b --- /dev/null +++ b/src/test/java/org/dataloader/DataLoaderCacheTest.java @@ -0,0 +1,392 @@ +package org.dataloader; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.awaitility.Awaitility.await; +import static org.dataloader.DataLoaderOptions.newOptions; +import static org.dataloader.TestKit.getJsonObjectCacheMapFn; +import static org.dataloader.TestKit.idLoader; +import static org.dataloader.TestKit.idLoaderBlowsUps; +import static org.dataloader.impl.CompletableFutureKit.cause; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertThat; + +public class DataLoaderCacheTest { + + @Test + public void should_Cache_repeated_requests() throws ExecutionException, InterruptedException { + List> loadCalls = new ArrayList<>(); + DataLoader identityLoader = idLoader(new DataLoaderOptions(), loadCalls); + + CompletableFuture future1 = identityLoader.load("A"); + CompletableFuture future2 = identityLoader.load("B"); + identityLoader.dispatch(); + + await().until(() -> future1.isDone() && future2.isDone()); + assertThat(future1.get(), equalTo("A")); + assertThat(future2.get(), equalTo("B")); + assertThat(loadCalls, equalTo(singletonList(asList("A", "B")))); + + CompletableFuture future1a = identityLoader.load("A"); + CompletableFuture future3 = identityLoader.load("C"); + identityLoader.dispatch(); + + await().until(() -> future1a.isDone() && future3.isDone()); + assertThat(future1a.get(), equalTo("A")); + assertThat(future3.get(), equalTo("C")); + assertThat(loadCalls, equalTo(asList(asList("A", "B"), singletonList("C")))); + + CompletableFuture future1b = identityLoader.load("A"); + CompletableFuture future2a = identityLoader.load("B"); + CompletableFuture future3a = identityLoader.load("C"); + identityLoader.dispatch(); + + await().until(() -> future1b.isDone() && future2a.isDone() && future3a.isDone()); + assertThat(future1b.get(), equalTo("A")); + assertThat(future2a.get(), equalTo("B")); + assertThat(future3a.get(), equalTo("C")); + assertThat(loadCalls, equalTo(asList(asList("A", "B"), singletonList("C")))); + } + + @Test + public void should_Accept_a_custom_cache_map_implementation() throws ExecutionException, InterruptedException { + CustomCacheMap customMap = new CustomCacheMap(); + List> loadCalls = new ArrayList<>(); + DataLoaderOptions options = newOptions().setCacheMap(customMap); + DataLoader identityLoader = idLoader(options, loadCalls); + + // Fetches as expected + + CompletableFuture future1 = identityLoader.load("a"); + CompletableFuture future2 = identityLoader.load("b"); + CompletableFuture> composite = identityLoader.dispatch().getPromisedResults(); + + await().until(composite::isDone); + assertThat(future1.get(), equalTo("a")); + assertThat(future2.get(), equalTo("b")); + + assertThat(loadCalls, equalTo(singletonList(asList("a", "b")))); + assertArrayEquals(customMap.stash.keySet().toArray(), asList("a", "b").toArray()); + + CompletableFuture future3 = identityLoader.load("c"); + CompletableFuture future2a = identityLoader.load("b"); + composite = identityLoader.dispatch().getPromisedResults(); + + await().until(composite::isDone); + assertThat(future3.get(), equalTo("c")); + assertThat(future2a.get(), equalTo("b")); + + assertThat(loadCalls, equalTo(asList(asList("a", "b"), 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()); + + CompletableFuture future2b = identityLoader.load("b"); + composite = identityLoader.dispatch().getPromisedResults(); + + await().until(composite::isDone); + assertThat(future2b.get(), equalTo("b")); + assertThat(loadCalls, equalTo(asList(asList("a", "b"), + singletonList("c"), singletonList("b")))); + assertArrayEquals(customMap.stash.keySet().toArray(), asList("a", "c", "b").toArray()); + + // Supports clear all + + identityLoader.clearAll(); + assertArrayEquals(customMap.stash.keySet().toArray(), emptyList().toArray()); + } + + @Test + public void should_allow_values_extracted_from_cache_on_load() { + CustomCacheMap customMap = new CustomCacheMap(); + List> loadCalls = new ArrayList<>(); + DataLoaderOptions options = newOptions().setCacheMap(customMap); + DataLoader identityLoader = idLoader(options, loadCalls); + + customMap.set("a", "cachedVal"); // will prevent a batch load + + CompletableFuture future1 = identityLoader.load("a"); + CompletableFuture future2 = identityLoader.load("b"); + CompletableFuture> composite = identityLoader.dispatch().getPromisedResults(); + + await().until(composite::isDone); + assertThat(future1.join(), equalTo("cachedVal")); + assertThat(future2.join(), equalTo("b")); + + assertThat(loadCalls, equalTo(singletonList(singletonList("b")))); + assertArrayEquals(customMap.stash.keySet().toArray(), asList("a", "b").toArray()); + } + + @Test + public void should_allow_promise_map_to_be_used() { + CustomCacheMap customMap = new CustomCacheMap(); + List> loadCalls = new ArrayList<>(); + DataLoaderOptions options = newOptions().setPromiseCacheMap(customMap); + DataLoader identityLoader = idLoader(options, loadCalls); + + customMap.set("a", CompletableFuture.completedFuture("customValue")); + + CompletableFuture future1 = identityLoader.load("a"); + CompletableFuture future2 = identityLoader.load("b"); + CompletableFuture> composite = identityLoader.dispatch().getPromisedResults(); + + await().until(composite::isDone); + assertThat(future1.join(), equalTo("customValue")); + assertThat(future2.join(), equalTo("b")); + + assertThat(loadCalls, equalTo(singletonList(singletonList("b")))); + assertArrayEquals(customMap.stash.keySet().toArray(), asList("a", "b").toArray()); + } + + @Test + public void should_Cache_on_redispatch() throws ExecutionException, InterruptedException { + List> loadCalls = new ArrayList<>(); + DataLoader identityLoader = idLoader(new DataLoaderOptions(), loadCalls); + + CompletableFuture future1 = identityLoader.load("A"); + identityLoader.dispatch(); + + CompletableFuture> future2 = identityLoader.loadMany(asList("A", "B")); + identityLoader.dispatch(); + + await().until(() -> future1.isDone() && future2.isDone()); + assertThat(future1.get(), equalTo("A")); + assertThat(future2.get(), equalTo(asList("A", "B"))); + assertThat(loadCalls, equalTo(asList(singletonList("A"), singletonList("B")))); + } + + @Test + public void should_Clear_single_value_in_loader() throws ExecutionException, InterruptedException { + List> loadCalls = new ArrayList<>(); + DataLoader identityLoader = idLoader(new DataLoaderOptions(), loadCalls); + + CompletableFuture future1 = identityLoader.load("A"); + CompletableFuture future2 = identityLoader.load("B"); + identityLoader.dispatch(); + + await().until(() -> future1.isDone() && future2.isDone()); + assertThat(future1.get(), equalTo("A")); + assertThat(future2.get(), equalTo("B")); + assertThat(loadCalls, equalTo(singletonList(asList("A", "B")))); + + identityLoader.clear("A"); + + CompletableFuture future1a = identityLoader.load("A"); + CompletableFuture future2a = identityLoader.load("B"); + identityLoader.dispatch(); + + await().until(() -> future1a.isDone() && future2a.isDone()); + assertThat(future1a.get(), equalTo("A")); + assertThat(future2a.get(), equalTo("B")); + assertThat(loadCalls, equalTo(asList(asList("A", "B"), singletonList("A")))); + } + + @Test + public void should_Clear_all_values_in_loader() throws ExecutionException, InterruptedException { + List> loadCalls = new ArrayList<>(); + DataLoader identityLoader = idLoader(new DataLoaderOptions(), loadCalls); + + CompletableFuture future1 = identityLoader.load("A"); + CompletableFuture future2 = identityLoader.load("B"); + identityLoader.dispatch(); + + await().until(() -> future1.isDone() && future2.isDone()); + assertThat(future1.get(), equalTo("A")); + assertThat(future2.get(), equalTo("B")); + assertThat(loadCalls, equalTo(singletonList(asList("A", "B")))); + + identityLoader.clearAll(); + + CompletableFuture future1a = identityLoader.load("A"); + CompletableFuture future2a = identityLoader.load("B"); + identityLoader.dispatch(); + + await().until(() -> future1a.isDone() && future2a.isDone()); + assertThat(future1a.get(), equalTo("A")); + assertThat(future2a.get(), equalTo("B")); + assertThat(loadCalls, equalTo(asList(asList("A", "B"), asList("A", "B")))); + } + + @Test + public void should_Allow_priming_the_cache() throws ExecutionException, InterruptedException { + List> loadCalls = new ArrayList<>(); + DataLoader identityLoader = idLoader(new DataLoaderOptions(), loadCalls); + + identityLoader.prime("A", "A"); + + CompletableFuture future1 = identityLoader.load("A"); + CompletableFuture future2 = identityLoader.load("B"); + identityLoader.dispatch(); + + await().until(() -> future1.isDone() && future2.isDone()); + assertThat(future1.get(), equalTo("A")); + assertThat(future2.get(), equalTo("B")); + assertThat(loadCalls, equalTo(singletonList(singletonList("B")))); + } + + @Test + public void should_Not_prime_keys_that_already_exist() throws ExecutionException, InterruptedException { + List> loadCalls = new ArrayList<>(); + DataLoader identityLoader = idLoader(new DataLoaderOptions(), loadCalls); + + identityLoader.prime("A", "X"); + + CompletableFuture future1 = identityLoader.load("A"); + CompletableFuture future2 = identityLoader.load("B"); + CompletableFuture> composite = identityLoader.dispatch().getPromisedResults(); + + await().until(composite::isDone); + assertThat(future1.get(), equalTo("X")); + assertThat(future2.get(), equalTo("B")); + + identityLoader.prime("A", "Y"); + identityLoader.prime("B", "Y"); + + CompletableFuture future1a = identityLoader.load("A"); + CompletableFuture future2a = identityLoader.load("B"); + CompletableFuture> composite2 = identityLoader.dispatch().getPromisedResults(); + + await().until(composite2::isDone); + assertThat(future1a.get(), equalTo("X")); + assertThat(future2a.get(), equalTo("B")); + assertThat(loadCalls, equalTo(singletonList(singletonList("B")))); + } + + @Test + public void should_Allow_to_forcefully_prime_the_cache() throws ExecutionException, InterruptedException { + List> loadCalls = new ArrayList<>(); + DataLoader identityLoader = idLoader(new DataLoaderOptions(), loadCalls); + + identityLoader.prime("A", "X"); + + CompletableFuture future1 = identityLoader.load("A"); + CompletableFuture future2 = identityLoader.load("B"); + CompletableFuture> composite = identityLoader.dispatch().getPromisedResults(); + + await().until(composite::isDone); + assertThat(future1.get(), equalTo("X")); + assertThat(future2.get(), equalTo("B")); + + identityLoader.clear("A").prime("A", "Y"); + identityLoader.clear("B").prime("B", "Y"); + + CompletableFuture future1a = identityLoader.load("A"); + CompletableFuture future2a = identityLoader.load("B"); + CompletableFuture> composite2 = identityLoader.dispatch().getPromisedResults(); + + await().until(composite2::isDone); + assertThat(future1a.get(), equalTo("Y")); + assertThat(future2a.get(), equalTo("Y")); + assertThat(loadCalls, equalTo(singletonList(singletonList("B")))); + } + + @Test + public void should_not_Cache_failed_fetches_on_complete_failure() { + List> loadCalls = new ArrayList<>(); + DataLoader errorLoader = idLoaderBlowsUps(new DataLoaderOptions(), loadCalls); + + CompletableFuture future1 = errorLoader.load(1); + errorLoader.dispatch(); + + await().until(future1::isDone); + assertThat(future1.isCompletedExceptionally(), is(true)); + assertThat(cause(future1), instanceOf(IllegalStateException.class)); + + CompletableFuture future2 = errorLoader.load(1); + errorLoader.dispatch(); + + await().until(future2::isDone); + assertThat(future2.isCompletedExceptionally(), is(true)); + assertThat(cause(future2), instanceOf(IllegalStateException.class)); + assertThat(loadCalls, equalTo(asList(singletonList(1), singletonList(1)))); + } + + @Test + public void should_Clear_values_from_cache_after_errors() { + List> loadCalls = new ArrayList<>(); + DataLoader errorLoader = idLoaderBlowsUps(new DataLoaderOptions(), loadCalls); + + CompletableFuture future1 = errorLoader.load(1); + future1.handle((value, t) -> { + if (t != null) { + // Presumably determine if this error is transient, and only clear the cache in that case. + errorLoader.clear(1); + } + return null; + }); + errorLoader.dispatch(); + + await().until(future1::isDone); + assertThat(future1.isCompletedExceptionally(), is(true)); + assertThat(cause(future1), instanceOf(IllegalStateException.class)); + + CompletableFuture future2 = errorLoader.load(1); + future2.handle((value, t) -> { + if (t != null) { + // Again, only do this if you can determine the error is transient. + errorLoader.clear(1); + } + return null; + }); + errorLoader.dispatch(); + + await().until(future2::isDone); + assertThat(future2.isCompletedExceptionally(), is(true)); + assertThat(cause(future2), instanceOf(IllegalStateException.class)); + assertThat(loadCalls, equalTo(asList(singletonList(1), singletonList(1)))); + } + + + @Test + public void should_Allow_priming_the_cache_with_an_object_key() throws ExecutionException, InterruptedException { + List> loadCalls = new ArrayList<>(); + DataLoaderOptions options = newOptions().setCacheKeyFunction(getJsonObjectCacheMapFn()); + DataLoader identityLoader = idLoader(options, loadCalls); + + JsonObject key1 = new JsonObject().put("id", 123); + JsonObject key2 = new JsonObject().put("id", 123); + + identityLoader.prime(key1, key1); + + CompletableFuture future1 = identityLoader.load(key1); + CompletableFuture future2 = identityLoader.load(key2); + identityLoader.dispatch(); + + await().until(() -> future1.isDone() && future2.isDone()); + assertThat(loadCalls, equalTo(emptyList())); + assertThat(future1.get(), equalTo(key1)); + assertThat(future2.get(), equalTo(key1)); + } + + @Test + public void should_Handle_priming_the_cache_with_an_error() { + List> loadCalls = new ArrayList<>(); + DataLoader identityLoader = idLoader(new DataLoaderOptions(), loadCalls); + + identityLoader.prime(1, new IllegalStateException("Error")); + + CompletableFuture future1 = identityLoader.load(1); + identityLoader.dispatch(); + + await().until(future1::isDone); + assertThat(future1.isCompletedExceptionally(), is(true)); + assertThat(cause(future1), instanceOf(IllegalStateException.class)); + assertThat(loadCalls, equalTo(emptyList())); + } + +} diff --git a/src/test/java/org/dataloader/DataLoaderIfPresentTest.java b/src/test/java/org/dataloader/DataLoaderIfPresentTest.java index c015be6..1f58fed 100644 --- a/src/test/java/org/dataloader/DataLoaderIfPresentTest.java +++ b/src/test/java/org/dataloader/DataLoaderIfPresentTest.java @@ -6,6 +6,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; +import static org.dataloader.DataLoaderFactory.newDataLoader; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.sameInstance; import static org.junit.Assert.assertThat; @@ -23,7 +24,7 @@ private BatchLoader keysAsValues() { @Test public void should_detect_if_present_cf() { - DataLoader dataLoader = new DataLoader<>(keysAsValues()); + DataLoader dataLoader = DataLoaderFactory.newDataLoader(keysAsValues()); Optional> cachedPromise = dataLoader.getIfPresent(1); assertThat(cachedPromise.isPresent(), equalTo(false)); @@ -45,7 +46,7 @@ public void should_detect_if_present_cf() { @Test public void should_not_be_present_if_cleared() { - DataLoader dataLoader = new DataLoader<>(keysAsValues()); + DataLoader dataLoader = newDataLoader(keysAsValues()); dataLoader.load(1); @@ -64,7 +65,7 @@ public void should_not_be_present_if_cleared() { @Test public void should_allow_completed_cfs_to_be_found() { - DataLoader dataLoader = new DataLoader<>(keysAsValues()); + DataLoader dataLoader = newDataLoader(keysAsValues()); dataLoader.load(1); @@ -86,7 +87,7 @@ public void should_allow_completed_cfs_to_be_found() { @Test public void should_work_with_primed_caches() { - DataLoader dataLoader = new DataLoader<>(keysAsValues()); + DataLoader dataLoader = newDataLoader(keysAsValues()); dataLoader.prime(1, 666).prime(2, 999); Optional> cachedPromise = dataLoader.getIfPresent(1); diff --git a/src/test/java/org/dataloader/DataLoaderMapBatchLoaderTest.java b/src/test/java/org/dataloader/DataLoaderMapBatchLoaderTest.java index 1a436c1..6c0557f 100644 --- a/src/test/java/org/dataloader/DataLoaderMapBatchLoaderTest.java +++ b/src/test/java/org/dataloader/DataLoaderMapBatchLoaderTest.java @@ -53,12 +53,12 @@ private static DataLoader idMapLoader(DataLoaderOptions options, Li keys.forEach(k -> map.put(k, (V) k)); return CompletableFuture.completedFuture(map); }; - return DataLoader.newMappedDataLoader(kvBatchLoader, options); + return DataLoaderFactory.newMappedDataLoader(kvBatchLoader, options); } private static DataLoader idMapLoaderBlowsUps( DataLoaderOptions options, List> loadCalls) { - return new DataLoader<>((keys) -> { + return DataLoaderFactory.newDataLoader((keys) -> { loadCalls.add(new ArrayList<>(keys)); return futureError(); }, options); @@ -66,8 +66,8 @@ private static DataLoader idMapLoaderBlowsUps( @Test - public void basic_map_batch_loading() throws Exception { - DataLoader loader = DataLoader.newMappedDataLoader(evensOnlyMappedBatchLoader); + public void basic_map_batch_loading() { + DataLoader loader = DataLoaderFactory.newMappedDataLoader(evensOnlyMappedBatchLoader); loader.load("A"); loader.load("B"); @@ -96,7 +96,7 @@ public void should_map_Batch_multiple_requests() throws ExecutionException, Inte } @Test - public void can_split_max_batch_sizes_correctly() throws Exception { + public void can_split_max_batch_sizes_correctly() { List> loadCalls = new ArrayList<>(); DataLoader identityLoader = idMapLoader(newOptions().setMaxBatchSize(5), loadCalls); @@ -110,7 +110,7 @@ public void can_split_max_batch_sizes_correctly() throws Exception { expectedCalls.add(listFrom(15, 20)); expectedCalls.add(listFrom(20, 21)); - List result = identityLoader.dispatch().join(); + List result = identityLoader.dispatchAndJoin(); assertThat(result, equalTo(listFrom(0, 21))); assertThat(loadCalls, equalTo(expectedCalls)); diff --git a/src/test/java/org/dataloader/DataLoaderRegistryTest.java b/src/test/java/org/dataloader/DataLoaderRegistryTest.java index cd33ae3..4151756 100644 --- a/src/test/java/org/dataloader/DataLoaderRegistryTest.java +++ b/src/test/java/org/dataloader/DataLoaderRegistryTest.java @@ -6,6 +6,7 @@ import java.util.concurrent.CompletableFuture; import static java.util.Arrays.asList; +import static org.dataloader.DataLoaderFactory.newDataLoader; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItems; import static org.hamcrest.Matchers.sameInstance; @@ -15,10 +16,10 @@ public class DataLoaderRegistryTest { final BatchLoader identityBatchLoader = CompletableFuture::completedFuture; @Test - public void registration_works() throws Exception { - DataLoader dlA = new DataLoader<>(identityBatchLoader); - DataLoader dlB = new DataLoader<>(identityBatchLoader); - DataLoader dlC = new DataLoader<>(identityBatchLoader); + public void registration_works() { + DataLoader dlA = newDataLoader(identityBatchLoader); + DataLoader dlB = newDataLoader(identityBatchLoader); + DataLoader dlC = newDataLoader(identityBatchLoader); DataLoaderRegistry registry = new DataLoaderRegistry(); @@ -49,12 +50,12 @@ public void registration_works() throws Exception { } @Test - public void registries_can_be_combined() throws Exception { + public void registries_can_be_combined() { - DataLoader dlA = new DataLoader<>(identityBatchLoader); - DataLoader dlB = new DataLoader<>(identityBatchLoader); - DataLoader dlC = new DataLoader<>(identityBatchLoader); - DataLoader dlD = new DataLoader<>(identityBatchLoader); + DataLoader dlA = newDataLoader(identityBatchLoader); + DataLoader dlB = newDataLoader(identityBatchLoader); + DataLoader dlC = newDataLoader(identityBatchLoader); + DataLoader dlD = newDataLoader(identityBatchLoader); DataLoaderRegistry registry1 = new DataLoaderRegistry(); @@ -71,13 +72,13 @@ public void registries_can_be_combined() throws Exception { } @Test - public void stats_can_be_collected() throws Exception { + public void stats_can_be_collected() { DataLoaderRegistry registry = new DataLoaderRegistry(); - DataLoader dlA = new DataLoader<>(identityBatchLoader); - DataLoader dlB = new DataLoader<>(identityBatchLoader); - DataLoader dlC = new DataLoader<>(identityBatchLoader); + DataLoader dlA = newDataLoader(identityBatchLoader); + DataLoader dlB = newDataLoader(identityBatchLoader); + DataLoader dlC = newDataLoader(identityBatchLoader); registry.register("a", dlA).register("b", dlB).register("c", dlC); @@ -107,7 +108,7 @@ public void computeIfAbsent_creates_a_data_loader_if_there_was_no_value_at_key() DataLoaderRegistry registry = new DataLoaderRegistry(); - DataLoader dlA = new DataLoader<>(identityBatchLoader); + DataLoader dlA = newDataLoader(identityBatchLoader); DataLoader registered = registry.computeIfAbsent("a", (key) -> dlA); assertThat(registered, equalTo(dlA)); @@ -120,11 +121,11 @@ public void computeIfAbsent_returns_an_existing_data_loader_if_there_was_a_value DataLoaderRegistry registry = new DataLoaderRegistry(); - DataLoader dlA = new DataLoader<>(identityBatchLoader); + DataLoader dlA = newDataLoader(identityBatchLoader); registry.computeIfAbsent("a", (key) -> dlA); // register again at same key - DataLoader dlA2 = new DataLoader<>(identityBatchLoader); + DataLoader dlA2 = newDataLoader(identityBatchLoader); DataLoader registered = registry.computeIfAbsent("a", (key) -> dlA2); assertThat(registered, equalTo(dlA)); @@ -137,8 +138,8 @@ public void dispatch_counts_are_maintained() { DataLoaderRegistry registry = new DataLoaderRegistry(); - DataLoader dlA = new DataLoader<>(identityBatchLoader); - DataLoader dlB = new DataLoader<>(identityBatchLoader); + DataLoader dlA = newDataLoader(identityBatchLoader); + DataLoader dlB = newDataLoader(identityBatchLoader); registry.register("a", dlA); registry.register("b", dlB); @@ -151,7 +152,7 @@ public void dispatch_counts_are_maintained() { int dispatchDepth = registry.dispatchDepth(); assertThat(dispatchDepth, equalTo(4)); - int dispatchedCount = registry.dispatchAllWithCount(); + int dispatchedCount = registry.dispatchAll(); dispatchDepth = registry.dispatchDepth(); assertThat(dispatchedCount, equalTo(4)); assertThat(dispatchDepth, equalTo(0)); diff --git a/src/test/java/org/dataloader/DataLoaderStatsTest.java b/src/test/java/org/dataloader/DataLoaderStatsTest.java index c6a355b..1be2e8c 100644 --- a/src/test/java/org/dataloader/DataLoaderStatsTest.java +++ b/src/test/java/org/dataloader/DataLoaderStatsTest.java @@ -12,6 +12,7 @@ import static java.util.Arrays.asList; import static java.util.concurrent.CompletableFuture.completedFuture; +import static org.dataloader.DataLoaderFactory.newDataLoader; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; @@ -21,9 +22,9 @@ public class DataLoaderStatsTest { @Test - public void stats_are_collected_by_default() throws Exception { + public void stats_are_collected_by_default() { BatchLoader batchLoader = CompletableFuture::completedFuture; - DataLoader loader = new DataLoader<>(batchLoader); + DataLoader loader = newDataLoader(batchLoader); loader.load("A"); loader.load("B"); @@ -57,7 +58,7 @@ public void stats_are_collected_by_default() throws Exception { @Test - public void stats_are_collected_with_specified_collector() throws Exception { + public void stats_are_collected_with_specified_collector() { // lets prime it with some numbers so we know its ours StatisticsCollector collector = new SimpleStatisticsCollector(); collector.incrementLoadCount(); @@ -65,7 +66,7 @@ public void stats_are_collected_with_specified_collector() throws Exception { BatchLoader batchLoader = CompletableFuture::completedFuture; DataLoaderOptions loaderOptions = DataLoaderOptions.newOptions().setStatisticsCollector(() -> collector); - DataLoader loader = new DataLoader<>(batchLoader, loaderOptions); + DataLoader loader = newDataLoader(batchLoader, loaderOptions); loader.load("A"); loader.load("B"); @@ -98,12 +99,12 @@ public void stats_are_collected_with_specified_collector() throws Exception { } @Test - public void stats_are_collected_with_caching_disabled() throws Exception { + public void stats_are_collected_with_caching_disabled() { StatisticsCollector collector = new SimpleStatisticsCollector(); BatchLoader batchLoader = CompletableFuture::completedFuture; DataLoaderOptions loaderOptions = DataLoaderOptions.newOptions().setStatisticsCollector(() -> collector).setCachingEnabled(false); - DataLoader loader = new DataLoader<>(batchLoader, loaderOptions); + DataLoader loader = newDataLoader(batchLoader, loaderOptions); loader.load("A"); loader.load("B"); @@ -152,8 +153,8 @@ public void stats_are_collected_with_caching_disabled() throws Exception { }; @Test - public void stats_are_collected_on_exceptions() throws Exception { - DataLoader loader = DataLoader.newDataLoaderWithTry(batchLoaderThatBlows); + public void stats_are_collected_on_exceptions() { + DataLoader loader = DataLoaderFactory.newDataLoaderWithTry(batchLoaderThatBlows); loader.load("A"); loader.load("exception"); diff --git a/src/test/java/org/dataloader/DataLoaderTest.java b/src/test/java/org/dataloader/DataLoaderTest.java index 0718225..6efbcdc 100644 --- a/src/test/java/org/dataloader/DataLoaderTest.java +++ b/src/test/java/org/dataloader/DataLoaderTest.java @@ -35,14 +35,17 @@ import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static org.awaitility.Awaitility.await; +import static org.dataloader.DataLoaderFactory.newDataLoader; import static org.dataloader.DataLoaderOptions.newOptions; +import static org.dataloader.TestKit.getJsonObjectCacheMapFn; +import static org.dataloader.TestKit.idLoader; +import static org.dataloader.TestKit.idLoaderBlowsUps; import static org.dataloader.TestKit.listFrom; import static org.dataloader.impl.CompletableFutureKit.cause; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertThat; /** @@ -60,7 +63,7 @@ public class DataLoaderTest { @Test public void should_Build_a_really_really_simple_data_loader() { AtomicBoolean success = new AtomicBoolean(); - DataLoader identityLoader = new DataLoader<>(keysAsValues()); + DataLoader identityLoader = newDataLoader(keysAsValues()); CompletionStage future1 = identityLoader.load(1); @@ -75,7 +78,7 @@ public void should_Build_a_really_really_simple_data_loader() { @Test public void should_Support_loading_multiple_keys_in_one_call() { AtomicBoolean success = new AtomicBoolean(); - DataLoader identityLoader = new DataLoader<>(keysAsValues()); + DataLoader identityLoader = newDataLoader(keysAsValues()); CompletionStage> futureAll = identityLoader.loadMany(asList(1, 2)); futureAll.thenAccept(promisedValues -> { @@ -90,7 +93,7 @@ public void should_Support_loading_multiple_keys_in_one_call() { @Test public void should_Resolve_to_empty_list_when_no_keys_supplied() { AtomicBoolean success = new AtomicBoolean(); - DataLoader identityLoader = new DataLoader<>(keysAsValues()); + DataLoader identityLoader = newDataLoader(keysAsValues()); CompletableFuture> futureEmpty = identityLoader.loadMany(emptyList()); futureEmpty.thenAccept(promisedValues -> { assertThat(promisedValues.size(), is(0)); @@ -104,13 +107,13 @@ public void should_Resolve_to_empty_list_when_no_keys_supplied() { @Test public void should_Return_zero_entries_dispatched_when_no_keys_supplied() { AtomicBoolean success = new AtomicBoolean(); - DataLoader identityLoader = new DataLoader<>(keysAsValues()); + DataLoader identityLoader = newDataLoader(keysAsValues()); CompletableFuture> futureEmpty = identityLoader.loadMany(emptyList()); futureEmpty.thenAccept(promisedValues -> { assertThat(promisedValues.size(), is(0)); success.set(true); }); - DispatchResult dispatchResult = identityLoader.dispatchWithCounts(); + DispatchResult dispatchResult = identityLoader.dispatch(); await().untilAtomic(success, is(true)); assertThat(dispatchResult.getKeysCount(), equalTo(0)); } @@ -131,14 +134,14 @@ public void should_Batch_multiple_requests() throws ExecutionException, Interrup } @Test - public void should_Return_number_of_batched_entries() throws ExecutionException, InterruptedException { + public void should_Return_number_of_batched_entries() { List> loadCalls = new ArrayList<>(); DataLoader identityLoader = idLoader(new DataLoaderOptions(), loadCalls); CompletableFuture future1 = identityLoader.load(1); CompletableFuture future1a = identityLoader.load(1); CompletableFuture future2 = identityLoader.load(2); - DispatchResult dispatchResult = identityLoader.dispatchWithCounts(); + DispatchResult dispatchResult = identityLoader.dispatch(); await().until(() -> future1.isDone() && future2.isDone()); assertThat(dispatchResult.getKeysCount(), equalTo(2)); // its two because its the number dispatched (by key) not the load calls @@ -161,40 +164,6 @@ public void should_Coalesce_identical_requests() throws ExecutionException, Inte assertThat(loadCalls, equalTo(singletonList(singletonList(1)))); } - @Test - public void should_Cache_repeated_requests() throws ExecutionException, InterruptedException { - List> loadCalls = new ArrayList<>(); - DataLoader identityLoader = idLoader(new DataLoaderOptions(), loadCalls); - - CompletableFuture future1 = identityLoader.load("A"); - CompletableFuture future2 = identityLoader.load("B"); - identityLoader.dispatch(); - - await().until(() -> future1.isDone() && future2.isDone()); - assertThat(future1.get(), equalTo("A")); - assertThat(future2.get(), equalTo("B")); - assertThat(loadCalls, equalTo(singletonList(asList("A", "B")))); - - CompletableFuture future1a = identityLoader.load("A"); - CompletableFuture future3 = identityLoader.load("C"); - identityLoader.dispatch(); - - await().until(() -> future1a.isDone() && future3.isDone()); - assertThat(future1a.get(), equalTo("A")); - assertThat(future3.get(), equalTo("C")); - assertThat(loadCalls, equalTo(asList(asList("A", "B"), singletonList("C")))); - - CompletableFuture future1b = identityLoader.load("A"); - CompletableFuture future2a = identityLoader.load("B"); - CompletableFuture future3a = identityLoader.load("C"); - identityLoader.dispatch(); - - await().until(() -> future1b.isDone() && future2a.isDone() && future3a.isDone()); - assertThat(future1b.get(), equalTo("A")); - assertThat(future2a.get(), equalTo("B")); - assertThat(future3a.get(), equalTo("C")); - assertThat(loadCalls, equalTo(asList(asList("A", "B"), singletonList("C")))); - } @Test public void should_Not_redispatch_previous_load() throws ExecutionException, InterruptedException { @@ -213,168 +182,6 @@ public void should_Not_redispatch_previous_load() throws ExecutionException, Int assertThat(loadCalls, equalTo(asList(singletonList("A"), singletonList("B")))); } - @Test - public void should_Cache_on_redispatch() throws ExecutionException, InterruptedException { - List> loadCalls = new ArrayList<>(); - DataLoader identityLoader = idLoader(new DataLoaderOptions(), loadCalls); - - CompletableFuture future1 = identityLoader.load("A"); - identityLoader.dispatch(); - - CompletableFuture> future2 = identityLoader.loadMany(asList("A", "B")); - identityLoader.dispatch(); - - await().until(() -> future1.isDone() && future2.isDone()); - assertThat(future1.get(), equalTo("A")); - assertThat(future2.get(), equalTo(asList("A", "B"))); - assertThat(loadCalls, equalTo(asList(singletonList("A"), singletonList("B")))); - } - - @Test - public void should_Clear_single_value_in_loader() throws ExecutionException, InterruptedException { - List> loadCalls = new ArrayList<>(); - DataLoader identityLoader = idLoader(new DataLoaderOptions(), loadCalls); - - CompletableFuture future1 = identityLoader.load("A"); - CompletableFuture future2 = identityLoader.load("B"); - identityLoader.dispatch(); - - await().until(() -> future1.isDone() && future2.isDone()); - assertThat(future1.get(), equalTo("A")); - assertThat(future2.get(), equalTo("B")); - assertThat(loadCalls, equalTo(singletonList(asList("A", "B")))); - - identityLoader.clear("A"); - - CompletableFuture future1a = identityLoader.load("A"); - CompletableFuture future2a = identityLoader.load("B"); - identityLoader.dispatch(); - - await().until(() -> future1a.isDone() && future2a.isDone()); - assertThat(future1a.get(), equalTo("A")); - assertThat(future2a.get(), equalTo("B")); - assertThat(loadCalls, equalTo(asList(asList("A", "B"), singletonList("A")))); - } - - @Test - public void should_Clear_all_values_in_loader() throws ExecutionException, InterruptedException { - List> loadCalls = new ArrayList<>(); - DataLoader identityLoader = idLoader(new DataLoaderOptions(), loadCalls); - - CompletableFuture future1 = identityLoader.load("A"); - CompletableFuture future2 = identityLoader.load("B"); - identityLoader.dispatch(); - - await().until(() -> future1.isDone() && future2.isDone()); - assertThat(future1.get(), equalTo("A")); - assertThat(future2.get(), equalTo("B")); - assertThat(loadCalls, equalTo(singletonList(asList("A", "B")))); - - identityLoader.clearAll(); - - CompletableFuture future1a = identityLoader.load("A"); - CompletableFuture future2a = identityLoader.load("B"); - identityLoader.dispatch(); - - await().until(() -> future1a.isDone() && future2a.isDone()); - assertThat(future1a.get(), equalTo("A")); - assertThat(future2a.get(), equalTo("B")); - assertThat(loadCalls, equalTo(asList(asList("A", "B"), asList("A", "B")))); - } - - @Test - public void should_Allow_priming_the_cache() throws ExecutionException, InterruptedException { - List> loadCalls = new ArrayList<>(); - DataLoader identityLoader = idLoader(new DataLoaderOptions(), loadCalls); - - identityLoader.prime("A", "A"); - - CompletableFuture future1 = identityLoader.load("A"); - CompletableFuture future2 = identityLoader.load("B"); - identityLoader.dispatch(); - - await().until(() -> future1.isDone() && future2.isDone()); - assertThat(future1.get(), equalTo("A")); - assertThat(future2.get(), equalTo("B")); - assertThat(loadCalls, equalTo(singletonList(singletonList("B")))); - } - - @Test - public void should_Not_prime_keys_that_already_exist() throws ExecutionException, InterruptedException { - List> loadCalls = new ArrayList<>(); - DataLoader identityLoader = idLoader(new DataLoaderOptions(), loadCalls); - - identityLoader.prime("A", "X"); - - CompletableFuture future1 = identityLoader.load("A"); - CompletableFuture future2 = identityLoader.load("B"); - CompletableFuture> composite = identityLoader.dispatch(); - - await().until(composite::isDone); - assertThat(future1.get(), equalTo("X")); - assertThat(future2.get(), equalTo("B")); - - identityLoader.prime("A", "Y"); - identityLoader.prime("B", "Y"); - - CompletableFuture future1a = identityLoader.load("A"); - CompletableFuture future2a = identityLoader.load("B"); - CompletableFuture> composite2 = identityLoader.dispatch(); - - await().until(composite2::isDone); - assertThat(future1a.get(), equalTo("X")); - assertThat(future2a.get(), equalTo("B")); - assertThat(loadCalls, equalTo(singletonList(singletonList("B")))); - } - - @Test - public void should_Allow_to_forcefully_prime_the_cache() throws ExecutionException, InterruptedException { - List> loadCalls = new ArrayList<>(); - DataLoader identityLoader = idLoader(new DataLoaderOptions(), loadCalls); - - identityLoader.prime("A", "X"); - - CompletableFuture future1 = identityLoader.load("A"); - CompletableFuture future2 = identityLoader.load("B"); - CompletableFuture> composite = identityLoader.dispatch(); - - await().until(composite::isDone); - assertThat(future1.get(), equalTo("X")); - assertThat(future2.get(), equalTo("B")); - - identityLoader.clear("A").prime("A", "Y"); - identityLoader.clear("B").prime("B", "Y"); - - CompletableFuture future1a = identityLoader.load("A"); - CompletableFuture future2a = identityLoader.load("B"); - CompletableFuture> composite2 = identityLoader.dispatch(); - - await().until(composite2::isDone); - assertThat(future1a.get(), equalTo("Y")); - assertThat(future2a.get(), equalTo("Y")); - assertThat(loadCalls, equalTo(singletonList(singletonList("B")))); - } - - @Test - public void should_not_Cache_failed_fetches_on_complete_failure() { - List> loadCalls = new ArrayList<>(); - DataLoader errorLoader = idLoaderBlowsUps(new DataLoaderOptions(), loadCalls); - - CompletableFuture future1 = errorLoader.load(1); - errorLoader.dispatch(); - - await().until(future1::isDone); - assertThat(future1.isCompletedExceptionally(), is(true)); - assertThat(cause(future1), instanceOf(IllegalStateException.class)); - - CompletableFuture future2 = errorLoader.load(1); - errorLoader.dispatch(); - - await().until(future2::isDone); - assertThat(future2.isCompletedExceptionally(), is(true)); - assertThat(cause(future2), instanceOf(IllegalStateException.class)); - assertThat(loadCalls, equalTo(asList(singletonList(1), singletonList(1)))); - } @Test public void should_Resolve_to_error_to_indicate_failure() throws ExecutionException, InterruptedException { @@ -408,7 +215,7 @@ public void should_Represent_failures_and_successes_simultaneously() throws Exec CompletableFuture future2 = evenLoader.load(2); CompletableFuture future3 = evenLoader.load(3); CompletableFuture future4 = evenLoader.load(4); - CompletableFuture> result = evenLoader.dispatch(); + CompletableFuture> result = evenLoader.dispatch().getPromisedResults(); result.thenAccept(promisedValues -> success.set(true)); await().untilAtomic(success, is(true)); @@ -472,56 +279,6 @@ public void should_NOT_Cache_failed_fetches_if_told_not_too() { // Accepts object key in custom cacheKey function - @Test - public void should_Handle_priming_the_cache_with_an_error() { - List> loadCalls = new ArrayList<>(); - DataLoader identityLoader = idLoader(new DataLoaderOptions(), loadCalls); - - identityLoader.prime(1, new IllegalStateException("Error")); - - CompletableFuture future1 = identityLoader.load(1); - identityLoader.dispatch(); - - await().until(future1::isDone); - assertThat(future1.isCompletedExceptionally(), is(true)); - assertThat(cause(future1), instanceOf(IllegalStateException.class)); - assertThat(loadCalls, equalTo(emptyList())); - } - - @Test - public void should_Clear_values_from_cache_after_errors() { - List> loadCalls = new ArrayList<>(); - DataLoader errorLoader = idLoaderBlowsUps(new DataLoaderOptions(), loadCalls); - - CompletableFuture future1 = errorLoader.load(1); - future1.handle((value, t) -> { - if (t != null) { - // Presumably determine if this error is transient, and only clear the cache in that case. - errorLoader.clear(1); - } - return null; - }); - errorLoader.dispatch(); - - await().until(future1::isDone); - assertThat(future1.isCompletedExceptionally(), is(true)); - assertThat(cause(future1), instanceOf(IllegalStateException.class)); - - CompletableFuture future2 = errorLoader.load(1); - future2.handle((value, t) -> { - if (t != null) { - // Again, only do this if you can determine the error is transient. - errorLoader.clear(1); - } - return null; - }); - errorLoader.dispatch(); - - await().until(future2::isDone); - assertThat(future2.isCompletedExceptionally(), is(true)); - assertThat(cause(future2), instanceOf(IllegalStateException.class)); - assertThat(loadCalls, equalTo(asList(singletonList(1), singletonList(1)))); - } @Test public void should_Propagate_error_to_all_loads() { @@ -559,7 +316,7 @@ public void should_Accept_objects_as_keys() { identityLoader.load(keyA); identityLoader.load(keyB); - identityLoader.dispatch().thenAccept(promisedValues -> { + identityLoader.dispatch().getPromisedResults().thenAccept(promisedValues -> { assertThat(promisedValues.get(0), equalTo(keyA)); assertThat(promisedValues.get(1), equalTo(keyB)); }); @@ -577,7 +334,7 @@ public void should_Accept_objects_as_keys() { identityLoader.load(keyA); identityLoader.load(keyB); - identityLoader.dispatch().thenAccept(promisedValues -> { + identityLoader.dispatch().getPromisedResults().thenAccept(promisedValues -> { assertThat(promisedValues.get(0), equalTo(keyA)); assertThat(identityLoader.getCacheKey(keyB), equalTo(keyB)); }); @@ -729,80 +486,9 @@ public void should_Accept_objects_with_different_order_of_keys() throws Executio assertThat(future2.get(), equalTo(key1)); } - @Test - public void should_Allow_priming_the_cache_with_an_object_key() throws ExecutionException, InterruptedException { - List> loadCalls = new ArrayList<>(); - DataLoaderOptions options = newOptions().setCacheKeyFunction(getJsonObjectCacheMapFn()); - DataLoader identityLoader = idLoader(options, loadCalls); - - JsonObject key1 = new JsonObject().put("id", 123); - JsonObject key2 = new JsonObject().put("id", 123); - - identityLoader.prime(key1, key1); - - CompletableFuture future1 = identityLoader.load(key1); - CompletableFuture future2 = identityLoader.load(key2); - identityLoader.dispatch(); - - await().until(() -> future1.isDone() && future2.isDone()); - assertThat(loadCalls, equalTo(emptyList())); - assertThat(future1.get(), equalTo(key1)); - assertThat(future2.get(), equalTo(key1)); - } - - @Test - public void should_Accept_a_custom_cache_map_implementation() throws ExecutionException, InterruptedException { - CustomCacheMap customMap = new CustomCacheMap(); - List> loadCalls = new ArrayList<>(); - DataLoaderOptions options = newOptions().setCacheMap(customMap); - DataLoader identityLoader = idLoader(options, loadCalls); - - // Fetches as expected - - CompletableFuture future1 = identityLoader.load("a"); - CompletableFuture future2 = identityLoader.load("b"); - CompletableFuture> composite = identityLoader.dispatch(); - - await().until(composite::isDone); - assertThat(future1.get(), equalTo("a")); - assertThat(future2.get(), equalTo("b")); - - assertThat(loadCalls, equalTo(singletonList(asList("a", "b")))); - assertArrayEquals(customMap.stash.keySet().toArray(), asList("a", "b").toArray()); - - CompletableFuture future3 = identityLoader.load("c"); - CompletableFuture future2a = identityLoader.load("b"); - composite = identityLoader.dispatch(); - - await().until(composite::isDone); - assertThat(future3.get(), equalTo("c")); - assertThat(future2a.get(), equalTo("b")); - - assertThat(loadCalls, equalTo(asList(asList("a", "b"), 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()); - - CompletableFuture future2b = identityLoader.load("b"); - composite = identityLoader.dispatch(); - - await().until(composite::isDone); - assertThat(future2b.get(), equalTo("b")); - assertThat(loadCalls, equalTo(asList(asList("a", "b"), - singletonList("c"), singletonList("b")))); - assertArrayEquals(customMap.stash.keySet().toArray(), asList("a", "c", "b").toArray()); - - // Supports clear all - - identityLoader.clearAll(); - assertArrayEquals(customMap.stash.keySet().toArray(), emptyList().toArray()); - } @Test - public void batching_disabled_should_dispatch_immediately() throws Exception { + public void batching_disabled_should_dispatch_immediately() { List> loadCalls = new ArrayList<>(); DataLoaderOptions options = newOptions().setBatchingEnabled(false); DataLoader identityLoader = idLoader(options, loadCalls); @@ -830,7 +516,7 @@ public void batching_disabled_should_dispatch_immediately() throws Exception { } @Test - public void batching_disabled_and_caching_disabled_should_dispatch_immediately_and_forget() throws Exception { + public void batching_disabled_and_caching_disabled_should_dispatch_immediately_and_forget() { List> loadCalls = new ArrayList<>(); DataLoaderOptions options = newOptions().setBatchingEnabled(false).setCachingEnabled(false); DataLoader identityLoader = idLoader(options, loadCalls); @@ -861,7 +547,7 @@ public void batching_disabled_and_caching_disabled_should_dispatch_immediately_a } @Test - public void batches_multiple_requests_with_max_batch_size() throws Exception { + public void batches_multiple_requests_with_max_batch_size() { List> loadCalls = new ArrayList<>(); DataLoader identityLoader = idLoader(newOptions().setMaxBatchSize(2), loadCalls); @@ -882,7 +568,7 @@ public void batches_multiple_requests_with_max_batch_size() throws Exception { } @Test - public void can_split_max_batch_sizes_correctly() throws Exception { + public void can_split_max_batch_sizes_correctly() { List> loadCalls = new ArrayList<>(); DataLoader identityLoader = idLoader(newOptions().setMaxBatchSize(5), loadCalls); @@ -896,7 +582,7 @@ public void can_split_max_batch_sizes_correctly() throws Exception { expectedCalls.add(listFrom(15, 20)); expectedCalls.add(listFrom(20, 21)); - List result = identityLoader.dispatch().join(); + List result = identityLoader.dispatch().getPromisedResults().join(); assertThat(result, equalTo(listFrom(0, 21))); assertThat(loadCalls, equalTo(expectedCalls)); @@ -938,19 +624,19 @@ public void should_Batch_loads_occurring_within_futures() { @Test public void can_call_a_loader_from_a_loader() throws Exception { List> deepLoadCalls = new ArrayList<>(); - DataLoader deepLoader = DataLoader.newDataLoader(keys -> { + DataLoader deepLoader = newDataLoader(keys -> { deepLoadCalls.add(keys); return CompletableFuture.completedFuture(keys); }); List> aLoadCalls = new ArrayList<>(); - DataLoader aLoader = new DataLoader<>(keys -> { + DataLoader aLoader = newDataLoader(keys -> { aLoadCalls.add(keys); return deepLoader.loadMany(keys); }); List> bLoadCalls = new ArrayList<>(); - DataLoader bLoader = new DataLoader<>(keys -> { + DataLoader bLoader = newDataLoader(keys -> { bLoadCalls.add(keys); return deepLoader.loadMany(keys); }); @@ -961,10 +647,10 @@ public void can_call_a_loader_from_a_loader() throws Exception { CompletableFuture b2 = bLoader.load("B2"); CompletableFuture.allOf( - aLoader.dispatch(), - deepLoader.dispatch(), - bLoader.dispatch(), - deepLoader.dispatch() + aLoader.dispatch().getPromisedResults(), + deepLoader.dispatch().getPromisedResults(), + bLoader.dispatch().getPromisedResults(), + deepLoader.dispatch().getPromisedResults() ).join(); assertThat(a1.get(), equalTo("A1")); @@ -983,7 +669,7 @@ public void can_call_a_loader_from_a_loader() throws Exception { } @Test - public void should_allow_composition_of_data_loader_calls() throws Exception { + public void should_allow_composition_of_data_loader_calls() { UserManager userManager = new UserManager(); BatchLoader userBatchLoader = userIds -> CompletableFuture @@ -991,7 +677,7 @@ public void should_allow_composition_of_data_loader_calls() throws Exception { .stream() .map(userManager::loadUserById) .collect(Collectors.toList())); - DataLoader userLoader = new DataLoader<>(userBatchLoader); + DataLoader userLoader = newDataLoader(userBatchLoader); AtomicBoolean gandalfCalled = new AtomicBoolean(false); AtomicBoolean sarumanCalled = new AtomicBoolean(false); @@ -1019,35 +705,9 @@ public void should_allow_composition_of_data_loader_calls() throws Exception { } - private static CacheKey getJsonObjectCacheMapFn() { - return key -> key.stream() - .map(entry -> entry.getKey() + ":" + entry.getValue()) - .sorted() - .collect(Collectors.joining()); - } - - private static DataLoader idLoader(DataLoaderOptions options, List> loadCalls) { - return DataLoader.newDataLoader(keys -> { - loadCalls.add(new ArrayList<>(keys)); - @SuppressWarnings("unchecked") - List values = keys.stream() - .map(k -> (V) k) - .collect(Collectors.toList()); - return CompletableFuture.completedFuture(values); - }, options); - } - - private static DataLoader idLoaderBlowsUps( - DataLoaderOptions options, List> loadCalls) { - return new DataLoader<>(keys -> { - loadCalls.add(new ArrayList<>(keys)); - return TestKit.futureError(); - }, options); - } - private static DataLoader idLoaderAllExceptions( DataLoaderOptions options, List> loadCalls) { - return new DataLoader<>(keys -> { + return newDataLoader(keys -> { loadCalls.add(new ArrayList<>(keys)); List errors = keys.stream().map(k -> new IllegalStateException("Error")).collect(Collectors.toList()); @@ -1057,7 +717,7 @@ private static DataLoader idLoaderAllExceptions( private static DataLoader idLoaderOddEvenExceptions( DataLoaderOptions options, List> loadCalls) { - return new DataLoader<>(keys -> { + return newDataLoader(keys -> { loadCalls.add(new ArrayList<>(keys)); List errors = new ArrayList<>(); diff --git a/src/test/java/org/dataloader/DataLoaderWithTryTest.java b/src/test/java/org/dataloader/DataLoaderWithTryTest.java index b2127e6..4f86bcf 100644 --- a/src/test/java/org/dataloader/DataLoaderWithTryTest.java +++ b/src/test/java/org/dataloader/DataLoaderWithTryTest.java @@ -12,6 +12,7 @@ import static java.util.Arrays.asList; import static java.util.Collections.singletonList; +import static org.dataloader.DataLoaderFactory.newMappedDataLoaderWithTry; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertThat; @@ -36,7 +37,7 @@ public void should_handle_Trys_coming_back_from_batchLoader() throws Exception { return CompletableFuture.completedFuture(result); }; - DataLoader dataLoader = DataLoader.newDataLoaderWithTry(batchLoader); + DataLoader dataLoader = DataLoaderFactory.newDataLoaderWithTry(batchLoader); commonTryAsserts(batchKeyCalls, dataLoader); } @@ -59,7 +60,7 @@ public void should_handle_Trys_coming_back_from_mapped_batchLoader() throws Exce return CompletableFuture.completedFuture(result); }; - DataLoader dataLoader = DataLoader.newMappedDataLoaderWithTry(batchLoader); + DataLoader dataLoader = newMappedDataLoaderWithTry(batchLoader); commonTryAsserts(batchKeyCalls, dataLoader); } diff --git a/src/test/java/org/dataloader/TestKit.java b/src/test/java/org/dataloader/TestKit.java index 82f73d6..dbac802 100644 --- a/src/test/java/org/dataloader/TestKit.java +++ b/src/test/java/org/dataloader/TestKit.java @@ -4,7 +4,9 @@ import java.util.Collection; import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import static org.dataloader.DataLoaderFactory.newDataLoader; import static org.dataloader.impl.CompletableFutureKit.failedFuture; public class TestKit { @@ -17,6 +19,33 @@ public static Collection listFrom(int i, int max) { return ints; } + public static DataLoader idLoader(DataLoaderOptions options, List> loadCalls) { + return DataLoaderFactory.newDataLoader(keys -> { + loadCalls.add(new ArrayList<>(keys)); + @SuppressWarnings("unchecked") + List values = keys.stream() + .map(k -> (V) k) + .collect(Collectors.toList()); + return CompletableFuture.completedFuture(values); + }, options); + } + + public static DataLoader idLoaderBlowsUps( + DataLoaderOptions options, List> loadCalls) { + return newDataLoader(keys -> { + loadCalls.add(new ArrayList<>(keys)); + return TestKit.futureError(); + }, options); + } + + public static CacheKey getJsonObjectCacheMapFn() { + return key -> key.stream() + .map(entry -> entry.getKey() + ":" + entry.getValue()) + .sorted() + .collect(Collectors.joining()); + } + + static CompletableFuture futureError() { return failedFuture(new IllegalStateException("Error")); }