From c08dd816fd5337b1cf7de3940d187f65d3dc3249 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Mon, 9 Mar 2020 21:58:19 +1100 Subject: [PATCH 1/3] Initial cut of the DataLoader as an interface --- src/main/java/org/dataloader/DataLoader.java | 534 +----------------- .../org/dataloader/DataLoaderFactory.java | 258 +++++++++ .../org/dataloader/DataLoaderOptions.java | 22 +- .../dataloader/impl/CompletableFutureKit.java | 6 +- .../{ => impl}/DataLoaderHelper.java | 26 +- .../org/dataloader/impl/DataLoaderImpl.java | 320 +++++++++++ src/test/java/ReadmeExamples.java | 17 +- .../DataLoaderBatchLoaderEnvironmentTest.java | 32 +- .../dataloader/DataLoaderIfPresentTest.java | 9 +- .../DataLoaderMapBatchLoaderTest.java | 10 +- .../dataloader/DataLoaderRegistryTest.java | 37 +- .../org/dataloader/DataLoaderStatsTest.java | 17 +- .../java/org/dataloader/DataLoaderTest.java | 47 +- .../org/dataloader/DataLoaderWithTryTest.java | 5 +- 14 files changed, 716 insertions(+), 624 deletions(-) create mode 100644 src/main/java/org/dataloader/DataLoaderFactory.java rename src/main/java/org/dataloader/{ => impl}/DataLoaderHelper.java (95%) create mode 100644 src/main/java/org/dataloader/impl/DataLoaderImpl.java diff --git a/src/main/java/org/dataloader/DataLoader.java b/src/main/java/org/dataloader/DataLoader.java index a088070..ac8e776 100644 --- a/src/main/java/org/dataloader/DataLoader.java +++ b/src/main/java/org/dataloader/DataLoader.java @@ -1,33 +1,11 @@ -/* - * 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.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 +24,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,465 +34,29 @@ * @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); - } +public interface DataLoader { + CompletableFuture load(K key); - /** - * 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); - } + CompletableFuture load(K key, Object keyContext); - /** - * 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); - } + CompletableFuture> loadMany(List keys); - /** - * 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); - } + CompletableFuture> loadMany(List keys, List keyContexts); - /** - * 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); - } + Optional> getIfPresent(K key); - 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()); + Optional> getIfCompleted(K key); - 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(); - } + CompletableFuture> dispatch(); - /** - * 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 - * 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); - } + DispatchResult dispatchWithCounts(); - /** - * 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); - } + List dispatchAndJoin(); - /** - * 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); - } + int dispatchDepth(); - - /** - * 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 - * start batch execution. If you forget this call the future will never be completed (unless already completed, - * and returned from cache). - *

- * The key context object may be useful in the batch loader interfaces such as {@link org.dataloader.BatchLoaderWithContext} or - * {@link org.dataloader.MappedBatchLoaderWithContext} to help retrieve data. - * - * @param key the key to load - * @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); - } - - /** - * 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 - * 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()); - } - - /** - * 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 - * start batch execution. If you forget this call the future will never be completed (unless already completed, - * and returned from cache). - *

- * The key context object may be useful in the batch loader interfaces such as {@link org.dataloader.BatchLoaderWithContext} or - * {@link org.dataloader.MappedBatchLoaderWithContext} to help retrieve data. - * - * @param keys the list of keys to load - * @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); - - 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); - } - } - - /** - * Dispatches the queued load requests to the batch execution function and returns a promise of the result. - *

- * If batching is disabled, or there are no queued requests, then a succeeded promise is returned. - * - * @return the promise of the queued load requests - */ - public CompletableFuture> dispatch() { - return helper.dispatch().getPromisedResults(); - } - - /** - * Dispatches the queued load requests to the batch execution function and returns both the promise of the result - * 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 - * returned. - * - * @return the promise of the queued load requests and the number of keys dispatched. - */ - public DispatchResult dispatchWithCounts() { - return helper.dispatch(); - } - - /** - * Normally {@link #dispatch()} is an asynchronous operation but this version will 'join' on the - * results if dispatch and wait for them to complete. If the {@link CompletableFuture} callbacks make more - * calls to this data loader then the {@link #dispatchDepth()} will be > 0 and this method will loop - * around and wait for any other extra batch loads to occur. - * - * @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; - } - - - /** - * @return the depth of the batched key loads that need to be dispatched - */ - public int dispatchDepth() { - return helper.dispatchDepth(); - } + Statistics getStatistics(); /** @@ -523,25 +66,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 +82,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 +91,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 +102,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..f5ccaba --- /dev/null +++ b/src/main/java/org/dataloader/DataLoaderFactory.java @@ -0,0 +1,258 @@ +package org.dataloader; + + +import org.dataloader.impl.DataLoaderImpl; + +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) + */ + @SuppressWarnings("unchecked") + 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..c3d9896 100644 --- a/src/main/java/org/dataloader/DataLoaderOptions.java +++ b/src/main/java/org/dataloader/DataLoaderOptions.java @@ -37,8 +37,8 @@ public class DataLoaderOptions { private boolean batchingEnabled; private boolean cachingEnabled; private boolean cachingExceptionsEnabled; - private CacheKey cacheKeyFunction; - private CacheMap cacheMap; + private CacheKey cacheKeyFunction; + private CacheMap cacheMap; private int maxBatchSize; private Supplier statisticsCollector; private BatchLoaderContextProvider environmentProvider; @@ -92,7 +92,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 +112,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 +121,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 +134,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,10 +159,9 @@ 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; } @@ -177,7 +173,7 @@ public DataLoaderOptions setCacheKeyFunction(CacheKey cacheKeyFunction) { * * @return an optional with the cache map instance, or empty */ - public Optional cacheMap() { + public Optional> cacheMap() { return Optional.ofNullable(cacheMap); } @@ -185,10 +181,9 @@ public Optional cacheMap() { * Sets the cache map implementation to use for 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; } @@ -208,7 +203,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 +223,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 +241,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/impl/CompletableFutureKit.java b/src/main/java/org/dataloader/impl/CompletableFutureKit.java index d4b9f79..d6bc863 100644 --- a/src/main/java/org/dataloader/impl/CompletableFutureKit.java +++ b/src/main/java/org/dataloader/impl/CompletableFutureKit.java @@ -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/DataLoaderHelper.java similarity index 95% rename from src/main/java/org/dataloader/DataLoaderHelper.java rename to src/main/java/org/dataloader/impl/DataLoaderHelper.java index f2437be..54b32f0 100644 --- a/src/main/java/org/dataloader/DataLoaderHelper.java +++ b/src/main/java/org/dataloader/impl/DataLoaderHelper.java @@ -1,5 +1,16 @@ -package org.dataloader; - +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.Internal; +import org.dataloader.MappedBatchLoader; +import org.dataloader.MappedBatchLoaderWithContext; +import org.dataloader.Try; import org.dataloader.impl.CompletableFutureKit; import org.dataloader.stats.StatisticsCollector; @@ -151,7 +162,7 @@ DispatchResult dispatch() { 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 +183,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 +205,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 +223,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 +241,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); diff --git a/src/main/java/org/dataloader/impl/DataLoaderImpl.java b/src/main/java/org/dataloader/impl/DataLoaderImpl.java new file mode 100644 index 0000000..25f0cf0 --- /dev/null +++ b/src/main/java/org/dataloader/impl/DataLoaderImpl.java @@ -0,0 +1,320 @@ +/* + * 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.CacheMap; +import org.dataloader.DataLoader; +import org.dataloader.DataLoaderOptions; +import org.dataloader.DispatchResult; +import org.dataloader.Internal; +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; + +@Internal +public class DataLoaderImpl implements DataLoader { + + private final DataLoaderHelper helper; + private final DataLoaderOptions loaderOptions; + private final CacheMap> futureCache; + private final StatisticsCollector stats; + + + @Internal + public DataLoaderImpl(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(); + } + + /** + * 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 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 + */ + @Override + 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 + */ + @Override + 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 + */ + @Override + public Optional> getIfCompleted(K key) { + return helper.getIfCompleted(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 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). + *

+ * The key context object may be useful in the batch loader interfaces such as {@link org.dataloader.BatchLoaderWithContext} or + * {@link org.dataloader.MappedBatchLoaderWithContext} to help retrieve data. + * + * @param key the key to load + * @param keyContext a context object that is specific to this key + * @return the future of the value + */ + @Override + public CompletableFuture load(K key, Object keyContext) { + return helper.load(key, 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 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 + */ + @Override + public CompletableFuture> loadMany(List keys) { + return loadMany(keys, Collections.emptyList()); + } + + /** + * 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 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). + *

+ * The key context object may be useful in the batch loader interfaces such as {@link org.dataloader.BatchLoaderWithContext} or + * {@link org.dataloader.MappedBatchLoaderWithContext} to help retrieve data. + * + * @param keys the list of keys to load + * @param keyContexts the list of key calling context objects + * @return the composite future of the list of values + */ + @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); + } + } + + /** + * Dispatches the queued load requests to the batch execution function and returns a promise of the result. + *

+ * If batching is disabled, or there are no queued requests, then a succeeded promise is returned. + * + * @return the promise of the queued load requests + */ + @Override + public CompletableFuture> dispatch() { + return helper.dispatch().getPromisedResults(); + } + + /** + * Dispatches the queued load requests to the batch execution function and returns both the promise of the result + * 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 + * returned. + * + * @return the promise of the queued load requests and the number of keys dispatched. + */ + @Override + public DispatchResult dispatchWithCounts() { + return helper.dispatch(); + } + + /** + * Normally {@link #dispatch()} is an asynchronous operation but this version will 'join' on the + * results if dispatch and wait for them to complete. If the {@link CompletableFuture} callbacks make more + * calls to this data loader then the {@link #dispatchDepth()} will be > 0 and this method will loop + * around and wait for any other extra batch loads to occur. + * + * @return the list of all results when the {@link #dispatchDepth()} reached 0 + */ + @Override + public List dispatchAndJoin() { + + List joinedResults = dispatch().join(); + List results = new ArrayList<>(joinedResults); + while (this.dispatchDepth() > 0) { + joinedResults = dispatch().join(); + results.addAll(joinedResults); + } + return results; + } + + + /** + * @return the depth of the batched key loads that need to be dispatched + */ + @Override + public int dispatchDepth() { + return helper.dispatchDepth(); + } + + + /** + * Clears the future with the specified key from the cache, if caching is enabled, so it will be re-fetched + * on the next load request. + * + * @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; + } + + /** + * Clears the entire cache map of the loader. + * + * @return the data loader for fluent coding + */ + public DataLoader clearAll() { + synchronized (this) { + futureCache.clear(); + } + return this; + } + + /** + * Primes the cache with the given key and value. + * + * @param key the key + * @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; + } + + /** + * Primes the cache with the given key and error. + * + * @param key the key + * @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; + } + + /** + * Gets the object that is used in the internal cache map as key, by applying the cache key function to + * the provided key. + *

+ * If no cache key function is present in {@link DataLoaderOptions}, then the returned value equals the input key. + * + * @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 + */ + @Override + public Statistics getStatistics() { + return stats.getStatistics(); + } + +} 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/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..ccafbe7 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); diff --git a/src/test/java/org/dataloader/DataLoaderRegistryTest.java b/src/test/java/org/dataloader/DataLoaderRegistryTest.java index cd33ae3..e941d9f 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); 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..9d18f67 100644 --- a/src/test/java/org/dataloader/DataLoaderTest.java +++ b/src/test/java/org/dataloader/DataLoaderTest.java @@ -35,6 +35,7 @@ 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.listFrom; import static org.dataloader.impl.CompletableFutureKit.cause; @@ -60,7 +61,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 +76,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 +91,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,7 +105,7 @@ 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)); @@ -131,7 +132,7 @@ 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); @@ -759,8 +760,8 @@ public void should_Accept_a_custom_cache_map_implementation() throws ExecutionEx // Fetches as expected - CompletableFuture future1 = identityLoader.load("a"); - CompletableFuture future2 = identityLoader.load("b"); + CompletableFuture future1 = identityLoader.load("a"); + CompletableFuture future2 = identityLoader.load("b"); CompletableFuture> composite = identityLoader.dispatch(); await().until(composite::isDone); @@ -770,8 +771,8 @@ public void should_Accept_a_custom_cache_map_implementation() throws ExecutionEx 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"); + CompletableFuture future3 = identityLoader.load("c"); + CompletableFuture future2a = identityLoader.load("b"); composite = identityLoader.dispatch(); await().until(composite::isDone); @@ -786,7 +787,7 @@ public void should_Accept_a_custom_cache_map_implementation() throws ExecutionEx identityLoader.clear("b"); assertArrayEquals(customMap.stash.keySet().toArray(), asList("a", "c").toArray()); - CompletableFuture future2b = identityLoader.load("b"); + CompletableFuture future2b = identityLoader.load("b"); composite = identityLoader.dispatch(); await().until(composite::isDone); @@ -802,7 +803,7 @@ public void should_Accept_a_custom_cache_map_implementation() throws ExecutionEx } @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 +831,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 +862,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 +883,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); @@ -938,19 +939,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); }); @@ -983,7 +984,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 +992,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); @@ -1027,7 +1028,7 @@ private static CacheKey getJsonObjectCacheMapFn() { } private static DataLoader idLoader(DataLoaderOptions options, List> loadCalls) { - return DataLoader.newDataLoader(keys -> { + return DataLoaderFactory.newDataLoader(keys -> { loadCalls.add(new ArrayList<>(keys)); @SuppressWarnings("unchecked") List values = keys.stream() @@ -1039,7 +1040,7 @@ private static DataLoader idLoader(DataLoaderOptions options, List< private static DataLoader idLoaderBlowsUps( DataLoaderOptions options, List> loadCalls) { - return new DataLoader<>(keys -> { + return newDataLoader(keys -> { loadCalls.add(new ArrayList<>(keys)); return TestKit.futureError(); }, options); @@ -1047,7 +1048,7 @@ private static DataLoader idLoaderBlowsUps( 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 +1058,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); } From cf0b40261b099c44765db5a63a47d959acbf4a03 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Tue, 10 Mar 2020 20:07:39 +1100 Subject: [PATCH 2/3] Broken out CacheMap so that its promises and values --- README.md | 6 +- src/main/java/org/dataloader/BatchLoader.java | 2 + .../BatchLoaderContextProvider.java | 2 + .../dataloader/BatchLoaderEnvironment.java | 7 +- .../BatchLoaderEnvironmentProvider.java | 2 + .../dataloader/BatchLoaderWithContext.java | 2 + src/main/java/org/dataloader/CacheMap.java | 11 +- src/main/java/org/dataloader/DataLoader.java | 111 ++++ .../org/dataloader/DataLoaderFactory.java | 4 +- .../org/dataloader/DataLoaderOptions.java | 50 +- .../org/dataloader/DataLoaderRegistry.java | 3 +- .../java/org/dataloader/DispatchResult.java | 2 + src/main/java/org/dataloader/Try.java | 2 + .../{ => annotations}/Internal.java | 2 +- .../{ => annotations}/PublicApi.java | 2 +- .../{ => annotations}/PublicSpi.java | 2 +- .../java/org/dataloader/impl/Assertions.java | 17 +- .../dataloader/impl/CompletableFutureKit.java | 2 +- .../org/dataloader/impl/DataLoaderHelper.java | 361 ------------ .../org/dataloader/impl/DataLoaderImpl.java | 527 ++++++++++++------ .../org/dataloader/impl/DefaultCacheMap.java | 2 +- .../org/dataloader/impl/PromisedValues.java | 16 +- .../dataloader/impl/PromisedValuesImpl.java | 6 +- .../java/org/dataloader/stats/Statistics.java | 2 +- .../dataloader/stats/StatisticsCollector.java | 2 +- .../org/dataloader/DataLoaderCacheTest.java | 392 +++++++++++++ .../java/org/dataloader/DataLoaderTest.java | 349 +----------- src/test/java/org/dataloader/TestKit.java | 29 + 28 files changed, 984 insertions(+), 931 deletions(-) rename src/main/java/org/dataloader/{ => annotations}/Internal.java (95%) rename src/main/java/org/dataloader/{ => annotations}/PublicApi.java (95%) rename src/main/java/org/dataloader/{ => annotations}/PublicSpi.java (96%) delete mode 100644 src/main/java/org/dataloader/impl/DataLoaderHelper.java create mode 100644 src/test/java/org/dataloader/DataLoaderCacheTest.java 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 ac8e776..d59b6f3 100644 --- a/src/main/java/org/dataloader/DataLoader.java +++ b/src/main/java/org/dataloader/DataLoader.java @@ -1,5 +1,6 @@ package org.dataloader; +import org.dataloader.annotations.PublicApi; import org.dataloader.stats.Statistics; import java.util.List; @@ -35,27 +36,137 @@ */ @PublicApi 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 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 + */ 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 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). + *

+ * The key context object may be useful in the batch loader interfaces such as {@link org.dataloader.BatchLoaderWithContext} or + * {@link org.dataloader.MappedBatchLoaderWithContext} to help retrieve data. + * + * @param key the key to load + * @param keyContext a context object that is specific to this key + * @return the future of the value + */ 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 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 + */ 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 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). + *

+ * The key context object may be useful in the batch loader interfaces such as {@link org.dataloader.BatchLoaderWithContext} or + * {@link org.dataloader.MappedBatchLoaderWithContext} to help retrieve data. + * + * @param keys the list of keys to load + * @param keyContexts the list of key calling context objects + * @return the composite future of the list of values + */ CompletableFuture> loadMany(List keys, List keyContexts); + /** + * 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); + /** + * 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 + */ Optional> getIfCompleted(K key); + /** + * Dispatches the queued load requests to the batch execution function and returns a promise of the result. + *

+ * If batching is disabled, or there are no queued requests, then a succeeded promise is returned. + * + * @return the promise of the queued load requests + */ CompletableFuture> dispatch(); + /** + * Dispatches the queued load requests to the batch execution function and returns both the promise of the result + * 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 + * returned. + * + * @return the promise of the queued load requests and the number of keys dispatched. + */ DispatchResult dispatchWithCounts(); + /** + * Normally {@link #dispatch()} is an asynchronous operation but this version will 'join' on the + * results if dispatch and wait for them to complete. If the {@link CompletableFuture} callbacks make more + * calls to this data loader then the {@link #dispatchDepth()} will be > 0 and this method will loop + * around and wait for any other extra batch loads to occur. + * + * @return the list of all results when the {@link #dispatchDepth()} reached 0 + */ List dispatchAndJoin(); + /** + * @return the depth of the batched key loads that need to be dispatched + */ 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(); diff --git a/src/main/java/org/dataloader/DataLoaderFactory.java b/src/main/java/org/dataloader/DataLoaderFactory.java index f5ccaba..dc27d27 100644 --- a/src/main/java/org/dataloader/DataLoaderFactory.java +++ b/src/main/java/org/dataloader/DataLoaderFactory.java @@ -3,6 +3,9 @@ import org.dataloader.impl.DataLoaderImpl; +/** + * This factory creates instances of {@link org.dataloader.DataLoader}s. + */ public class DataLoaderFactory { /** @@ -63,7 +66,6 @@ public static DataLoader newDataLoaderWithTry(BatchLoader * @return a new DataLoader * @see #newDataLoaderWithTry(BatchLoader) */ - @SuppressWarnings("unchecked") public static DataLoader newDataLoaderWithTry(BatchLoader> 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 c3d9896..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 { @@ -37,8 +39,9 @@ public class DataLoaderOptions { private boolean batchingEnabled; private boolean cachingEnabled; private boolean cachingExceptionsEnabled; - private CacheKey cacheKeyFunction; + private CacheKey cacheKeyFunction; 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; @@ -167,27 +171,61 @@ public DataLoaderOptions setCacheKeyFunction(CacheKey cacheKeyFunction) { } /** - * 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() { 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. + * + * @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 diff --git a/src/main/java/org/dataloader/DataLoaderRegistry.java b/src/main/java/org/dataloader/DataLoaderRegistry.java index bf9b2c6..3f32b0b 100644 --- a/src/main/java/org/dataloader/DataLoaderRegistry.java +++ b/src/main/java/org/dataloader/DataLoaderRegistry.java @@ -8,6 +8,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; +import org.dataloader.annotations.PublicApi; import org.dataloader.stats.Statistics; /** @@ -131,7 +132,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 d6bc863..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; diff --git a/src/main/java/org/dataloader/impl/DataLoaderHelper.java b/src/main/java/org/dataloader/impl/DataLoaderHelper.java deleted file mode 100644 index 54b32f0..0000000 --- a/src/main/java/org/dataloader/impl/DataLoaderHelper.java +++ /dev/null @@ -1,361 +0,0 @@ -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.Internal; -import org.dataloader.MappedBatchLoader; -import org.dataloader.MappedBatchLoaderWithContext; -import org.dataloader.Try; -import org.dataloader.impl.CompletableFutureKit; -import org.dataloader.stats.StatisticsCollector; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.stream.Collectors; - -import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; -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 { - - - class LoaderQueueEntry { - - final K key; - final V value; - final Object callContext; - - public LoaderQueueEntry(K key, V value, Object callContext) { - this.key = key; - this.value = value; - this.callContext = callContext; - } - - K getKey() { - return key; - } - - V getValue() { - return value; - } - - Object getCallContext() { - return callContext; - } - } - - private final DataLoader dataLoader; - private final Object batchLoadFunction; - private final DataLoaderOptions loaderOptions; - private final CacheMap> futureCache; - private final List>> loaderQueue; - private final StatisticsCollector stats; - - 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; - } - - Optional> getIfPresent(K key) { - synchronized (dataLoader) { - Object cacheKey = getCacheKey(nonNull(key)); - boolean cachingEnabled = loaderOptions.cachingEnabled(); - if (cachingEnabled) { - if (futureCache.containsKey(cacheKey)) { - stats.incrementCacheHitCount(); - return Optional.of(futureCache.get(cacheKey)); - } - } - } - 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(); - } - - - CompletableFuture load(K key, Object loadContext) { - synchronized (dataLoader) { - Object cacheKey = getCacheKey(nonNull(key)); - stats.incrementLoadCount(); - - boolean batchingEnabled = loaderOptions.batchingEnabled(); - boolean cachingEnabled = loaderOptions.cachingEnabled(); - - if (cachingEnabled) { - if (futureCache.containsKey(cacheKey)) { - stats.incrementCacheHitCount(); - return futureCache.get(cacheKey); - } - } - - CompletableFuture future = new CompletableFuture<>(); - if (batchingEnabled) { - loaderQueue.add(new LoaderQueueEntry<>(key, future, loadContext)); - } else { - stats.incrementBatchLoadCountBy(1); - // immediate execution of batch function - future = invokeLoaderImmediately(key, loadContext); - } - if (cachingEnabled) { - futureCache.set(cacheKey, future); - } - return future; - } - } - - @SuppressWarnings("unchecked") - Object getCacheKey(K key) { - return loaderOptions.cacheKeyFunction().isPresent() ? - loaderOptions.cacheKeyFunction().get().getKey(key) : key; - } - - DispatchResult dispatch() { - 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) { - loaderQueue.forEach(entry -> { - keys.add(entry.getKey()); - queuedFutures.add(entry.getValue()); - callContexts.add(entry.getCallContext()); - }); - loaderQueue.clear(); - } - if (!batchingEnabled || keys.isEmpty()) { - return new DispatchResult<>(CompletableFuture.completedFuture(emptyList()), 0); - } - final int totalEntriesHandled = keys.size(); - // - // order of keys -> values matter in data loader hence the use of linked hash map - // - // See https://github.com/facebook/dataloader/blob/master/README.md for more details - // - - // - // when the promised list of values completes, we transfer the values into - // the previously cached future objects that the client already has been given - // via calls to load("foo") and loadMany(["foo","bar"]) - // - int maxBatchSize = loaderOptions.maxBatchSize(); - CompletableFuture> futureList; - if (maxBatchSize > 0 && maxBatchSize < keys.size()) { - futureList = sliceIntoBatchesOfBatches(keys, queuedFutures, callContexts, maxBatchSize); - } else { - futureList = dispatchQueueBatch(keys, callContexts, queuedFutures); - } - return new DispatchResult<>(futureList, totalEntriesHandled); - } - - private CompletableFuture> sliceIntoBatchesOfBatches(List keys, List> queuedFutures, List callContexts, int maxBatchSize) { - // the number of keys is > than what the batch loader function can accept - // so make multiple calls to the loader - List>> allBatches = new ArrayList<>(); - int len = keys.size(); - int batchCount = (int) Math.ceil(len / (double) maxBatchSize); - for (int i = 0; i < batchCount; i++) { - - int fromIndex = i * maxBatchSize; - int toIndex = Math.min((i + 1) * maxBatchSize, len); - - List subKeys = keys.subList(fromIndex, toIndex); - List> subFutures = queuedFutures.subList(fromIndex, toIndex); - List subCallContexts = callContexts.subList(fromIndex, toIndex); - - allBatches.add(dispatchQueueBatch(subKeys, subCallContexts, subFutures)); - } - // - // now reassemble all the futures into one that is the complete set of results - return CompletableFuture.allOf(allBatches.toArray(new CompletableFuture[0])) - .thenApply(v -> allBatches.stream() - .map(CompletableFuture::join) - .flatMap(Collection::stream) - .collect(Collectors.toList())); - } - - @SuppressWarnings("unchecked") - private CompletableFuture> dispatchQueueBatch(List keys, List callContexts, List> queuedFutures) { - stats.incrementBatchLoadCountBy(keys.size()); - CompletionStage> batchLoad = invokeLoader(keys, callContexts); - return batchLoad - .toCompletableFuture() - .thenApply(values -> { - assertResultSize(keys, values); - - List clearCacheKeys = new ArrayList<>(); - for (int idx = 0; idx < queuedFutures.size(); idx++) { - V value = values.get(idx); - CompletableFuture future = queuedFutures.get(idx); - if (value instanceof Throwable) { - stats.incrementLoadErrorCount(); - future.completeExceptionally((Throwable) value); - clearCacheKeys.add(keys.get(idx)); - } else if (value instanceof Try) { - // we allow the batch loader to return a Try so we can better represent a computation - // that might have worked or not. - Try tryValue = (Try) value; - if (tryValue.isSuccess()) { - future.complete(tryValue.get()); - } else { - stats.incrementLoadErrorCount(); - future.completeExceptionally(tryValue.getThrowable()); - clearCacheKeys.add(keys.get(idx)); - } - } else { - future.complete(value); - } - } - possiblyClearCacheEntriesOnExceptions(clearCacheKeys); - return values; - }).exceptionally(ex -> { - stats.incrementBatchLoadExceptionCount(); - for (int idx = 0; idx < queuedFutures.size(); idx++) { - K key = keys.get(idx); - CompletableFuture future = queuedFutures.get(idx); - future.completeExceptionally(ex); - // clear any cached view of this key because they all failed - dataLoader.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"); - } - - private void possiblyClearCacheEntriesOnExceptions(List keys) { - if (keys.isEmpty()) { - return; - } - // by default we don't clear the cached view of this entry to avoid - // frequently loading the same error. This works for short lived request caches - // 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); - } - } - - - CompletableFuture invokeLoaderImmediately(K key, Object keyContext) { - List keys = singletonList(key); - CompletionStage singleLoadCall; - try { - Object context = loaderOptions.getBatchLoaderContextProvider().getContext(); - BatchLoaderEnvironment environment = BatchLoaderEnvironment.newBatchLoaderEnvironment() - .context(context).keyContexts(keys, singletonList(keyContext)).build(); - if (isMapLoader()) { - singleLoadCall = invokeMapBatchLoader(keys, environment).thenApply(list -> list.get(0)); - } else { - singleLoadCall = invokeListBatchLoader(keys, environment).thenApply(list -> list.get(0)); - } - return singleLoadCall.toCompletableFuture(); - } catch (Exception e) { - return CompletableFutureKit.failedFuture(e); - } - } - - CompletionStage> invokeLoader(List keys, List keyContexts) { - CompletionStage> batchLoad; - try { - Object context = loaderOptions.getBatchLoaderContextProvider().getContext(); - BatchLoaderEnvironment environment = BatchLoaderEnvironment.newBatchLoaderEnvironment() - .context(context).keyContexts(keys, keyContexts).build(); - if (isMapLoader()) { - batchLoad = invokeMapBatchLoader(keys, environment); - } else { - batchLoad = invokeListBatchLoader(keys, environment); - } - } catch (Exception e) { - batchLoad = CompletableFutureKit.failedFuture(e); - } - return batchLoad; - } - - @SuppressWarnings("unchecked") - private CompletionStage> invokeListBatchLoader(List keys, BatchLoaderEnvironment environment) { - CompletionStage> loadResult; - if (batchLoadFunction instanceof BatchLoaderWithContext) { - loadResult = ((BatchLoaderWithContext) batchLoadFunction).load(keys, environment); - } else { - loadResult = ((BatchLoader) batchLoadFunction).load(keys); - } - 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. - */ - @SuppressWarnings("unchecked") - private CompletionStage> invokeMapBatchLoader(List keys, BatchLoaderEnvironment environment) { - CompletionStage> loadResult; - Set setOfKeys = new LinkedHashSet<>(keys); - if (batchLoadFunction instanceof MappedBatchLoaderWithContext) { - loadResult = ((MappedBatchLoaderWithContext) batchLoadFunction).load(setOfKeys, environment); - } else { - loadResult = ((MappedBatchLoader) batchLoadFunction).load(setOfKeys); - } - CompletionStage> mapBatchLoad = nonNull(loadResult, "Your batch loader function MUST return a non null CompletionStage promise"); - return mapBatchLoad.thenApply(map -> { - List values = new ArrayList<>(); - for (K key : keys) { - V value = map.get(key); - values.add(value); - } - return values; - }); - } - - private boolean isMapLoader() { - return batchLoadFunction instanceof MappedBatchLoader || batchLoadFunction instanceof MappedBatchLoaderWithContext; - } - - int dispatchDepth() { - synchronized (dataLoader) { - return loaderQueue.size(); - } - } -} diff --git a/src/main/java/org/dataloader/impl/DataLoaderImpl.java b/src/main/java/org/dataloader/impl/DataLoaderImpl.java index 25f0cf0..6dfe0e5 100644 --- a/src/main/java/org/dataloader/impl/DataLoaderImpl.java +++ b/src/main/java/org/dataloader/impl/DataLoaderImpl.java @@ -13,152 +13,86 @@ * * 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.Internal; +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; import java.util.Optional; +import java.util.Set; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.stream.Collectors; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.dataloader.impl.Assertions.assertState; import static org.dataloader.impl.Assertions.nonNull; @Internal public class DataLoaderImpl implements DataLoader { - private final DataLoaderHelper helper; + private final Object batchLoadFunction; private final DataLoaderOptions loaderOptions; private final CacheMap> futureCache; + private final CacheMap valueCache; + private final List>> loaderQueue; private final StatisticsCollector stats; - @Internal public DataLoaderImpl(Object batchLoadFunction, DataLoaderOptions options) { - this.loaderOptions = options == null ? new DataLoaderOptions() : options; - this.futureCache = determineCacheMap(loaderOptions); + DataLoaderOptions loaderOptions = options == null ? new DataLoaderOptions() : options; // 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); + this.stats = nonNull(loaderOptions.getStatisticsCollector()); + this.batchLoadFunction = batchLoadFunction; + this.loaderOptions = loaderOptions; + this.futureCache = determinePromiseCacheMap(loaderOptions); + this.valueCache = determineValueCacheMap(loaderOptions); + this.loaderQueue = new ArrayList<>(); } - @SuppressWarnings("unchecked") - private CacheMap> determineCacheMap(DataLoaderOptions loaderOptions) { - return loaderOptions.cacheMap().isPresent() ? (CacheMap>) loaderOptions.cacheMap().get() : CacheMap.simpleMap(); - } - - /** - * 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 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 - */ @Override 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 - */ @Override 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 - */ + return getIfPresentImpl(key); + } + @Override public Optional> getIfCompleted(K key) { - return helper.getIfCompleted(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 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). - *

- * The key context object may be useful in the batch loader interfaces such as {@link org.dataloader.BatchLoaderWithContext} or - * {@link org.dataloader.MappedBatchLoaderWithContext} to help retrieve data. - * - * @param key the key to load - * @param keyContext a context object that is specific to this key - * @return the future of the value - */ + return getIfCompletedImpl(key); + } + @Override public CompletableFuture load(K key, Object keyContext) { - return helper.load(key, 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 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 - */ + return loadImpl(key, keyContext); + } + @Override public CompletableFuture> loadMany(List keys) { return loadMany(keys, Collections.emptyList()); } - /** - * 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 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). - *

- * The key context object may be useful in the batch loader interfaces such as {@link org.dataloader.BatchLoaderWithContext} or - * {@link org.dataloader.MappedBatchLoaderWithContext} to help retrieve data. - * - * @param keys the list of keys to load - * @param keyContexts the list of key calling context objects - * @return the composite future of the list of values - */ @Override public CompletableFuture> loadMany(List keys, List keyContexts) { nonNull(keys); @@ -178,40 +112,16 @@ public CompletableFuture> loadMany(List keys, List keyContext } } - /** - * Dispatches the queued load requests to the batch execution function and returns a promise of the result. - *

- * If batching is disabled, or there are no queued requests, then a succeeded promise is returned. - * - * @return the promise of the queued load requests - */ @Override public CompletableFuture> dispatch() { - return helper.dispatch().getPromisedResults(); + return dispatchImpl().getPromisedResults(); } - /** - * Dispatches the queued load requests to the batch execution function and returns both the promise of the result - * 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 - * returned. - * - * @return the promise of the queued load requests and the number of keys dispatched. - */ @Override public DispatchResult dispatchWithCounts() { - return helper.dispatch(); + return dispatchImpl(); } - /** - * Normally {@link #dispatch()} is an asynchronous operation but this version will 'join' on the - * results if dispatch and wait for them to complete. If the {@link CompletableFuture} callbacks make more - * calls to this data loader then the {@link #dispatchDepth()} will be > 0 and this method will loop - * around and wait for any other extra batch loads to occur. - * - * @return the list of all results when the {@link #dispatchDepth()} reached 0 - */ @Override public List dispatchAndJoin() { @@ -224,97 +134,360 @@ public List dispatchAndJoin() { return results; } - - /** - * @return the depth of the batched key loads that need to be dispatched - */ @Override public int dispatchDepth() { - return helper.dispatchDepth(); + synchronized (this) { + return loaderQueue.size(); + } } - - /** - * Clears the future with the specified key from the cache, if caching is enabled, so it will be re-fetched - * on the next load request. - * - * @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); + if (valueCache != null) { + valueCache.delete(cacheKey); + } } return this; } - /** - * Clears the entire cache map of the loader. - * - * @return the data loader for fluent coding - */ public DataLoader clearAll() { synchronized (this) { futureCache.clear(); + if (valueCache != null) { + valueCache.clear(); + } } return this; } - /** - * Primes the cache with the given key and value. - * - * @param key the key - * @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)); } + if (valueCache != null) { + if (!valueCache.containsKey(cacheKey)) { + valueCache.set(cacheKey, value); + } + } } return this; } - /** - * Primes the cache with the given key and error. - * - * @param key the key - * @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)); + synchronized (this) { + if (!futureCache.containsKey(cacheKey)) { + futureCache.set(cacheKey, CompletableFutureKit.failedFuture(error)); + } + if (valueCache != null) { + valueCache.delete(cacheKey); + } } return this; } - /** - * Gets the object that is used in the internal cache map as key, by applying the cache key function to - * the provided key. - *

- * If no cache key function is present in {@link DataLoaderOptions}, then the returned value equals the input key. - * - * @param key the input key - * @return the cache key after the input is transformed with the cache key function - */ + @SuppressWarnings("unchecked") public Object getCacheKey(K key) { - return helper.getCacheKey(key); + return loaderOptions.cacheKeyFunction() + .map(cacheKeyFunction -> cacheKeyFunction.getKey(key)) + .orElse(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 - */ @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)); + } + } + } + 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(); + + synchronized (this) { + stats.incrementLoadCount(); + + 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<>(); + if (batchingEnabled) { + loaderQueue.add(new LoaderQueueEntry<>(key, future, loadContext)); + } else { + stats.incrementBatchLoadCountBy(1); + // immediate execution of batch function + future = invokeLoaderImmediately(key, 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; + } + } + + 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 (this) { + loaderQueue.forEach(entry -> { + 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); + } + final int totalEntriesHandled = keys.size(); + // + // order of keys -> values matter in data loader hence the use of linked hash map + // + // See https://github.com/facebook/dataloader/blob/master/README.md for more details + // + + // + // when the promised list of values completes, we transfer the values into + // the previously cached future objects that the client already has been given + // via calls to load("foo") and loadMany(["foo","bar"]) + // + int maxBatchSize = loaderOptions.maxBatchSize(); + CompletableFuture> futureList; + if (maxBatchSize > 0 && maxBatchSize < keys.size()) { + futureList = sliceIntoBatchesOfBatches(keys, queuedFutures, callContexts, maxBatchSize); + } else { + futureList = dispatchQueueBatch(keys, callContexts, queuedFutures); + } + return new DispatchResult<>(futureList, totalEntriesHandled); + } + + private CompletableFuture> sliceIntoBatchesOfBatches(List keys, List> queuedFutures, List callContexts, int maxBatchSize) { + // the number of keys is > than what the batch loader function can accept + // so make multiple calls to the loader + List>> allBatches = new ArrayList<>(); + int len = keys.size(); + int batchCount = (int) Math.ceil(len / (double) maxBatchSize); + for (int i = 0; i < batchCount; i++) { + + int fromIndex = i * maxBatchSize; + int toIndex = Math.min((i + 1) * maxBatchSize, len); + + List subKeys = keys.subList(fromIndex, toIndex); + List> subFutures = queuedFutures.subList(fromIndex, toIndex); + List subCallContexts = callContexts.subList(fromIndex, toIndex); + + allBatches.add(dispatchQueueBatch(subKeys, subCallContexts, subFutures)); + } + // + // now reassemble all the futures into one that is the complete set of results + return CompletableFuture.allOf(allBatches.toArray(new CompletableFuture[0])) + .thenApply(v -> allBatches.stream() + .map(CompletableFuture::join) + .flatMap(Collection::stream) + .collect(Collectors.toList())); + } + + @SuppressWarnings("unchecked") + private CompletableFuture> dispatchQueueBatch(List keys, List callContexts, List> queuedFutures) { + stats.incrementBatchLoadCountBy(keys.size()); + CompletionStage> batchLoad = invokeLoader(keys, callContexts); + return batchLoad + .toCompletableFuture() + .thenApply(values -> { + assertResultSize(keys, values); + + List clearCacheKeys = new ArrayList<>(); + for (int idx = 0; idx < queuedFutures.size(); idx++) { + V value = values.get(idx); + CompletableFuture future = queuedFutures.get(idx); + if (value instanceof Throwable) { + stats.incrementLoadErrorCount(); + future.completeExceptionally((Throwable) value); + clearCacheKeys.add(keys.get(idx)); + } else if (value instanceof Try) { + // we allow the batch loader to return a Try so we can better represent a computation + // that might have worked or not. + Try tryValue = (Try) value; + if (tryValue.isSuccess()) { + future.complete(tryValue.get()); + } else { + stats.incrementLoadErrorCount(); + future.completeExceptionally(tryValue.getThrowable()); + clearCacheKeys.add(keys.get(idx)); + } + } else { + future.complete(value); + } + } + possiblyClearCacheEntriesOnExceptions(clearCacheKeys); + return values; + }).exceptionally(ex -> { + stats.incrementBatchLoadExceptionCount(); + for (int idx = 0; idx < queuedFutures.size(); idx++) { + K key = keys.get(idx); + CompletableFuture future = queuedFutures.get(idx); + future.completeExceptionally(ex); + // clear any cached view of this key because they all failed + 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"); + } + + private void possiblyClearCacheEntriesOnExceptions(List keys) { + if (keys.isEmpty()) { + return; + } + // by default we don't clear the cached view of this entry to avoid + // frequently loading the same error. This works for short lived request caches + // but might work against long lived caches. Hence we have an option that allows + // it to be cleared + if (!loaderOptions.cachingExceptionsEnabled()) { + keys.forEach(this::clear); + } + } + + CompletableFuture invokeLoaderImmediately(K key, Object keyContext) { + List keys = singletonList(key); + CompletionStage singleLoadCall; + try { + Object context = loaderOptions.getBatchLoaderContextProvider().getContext(); + BatchLoaderEnvironment environment = BatchLoaderEnvironment.newBatchLoaderEnvironment() + .context(context).keyContexts(keys, singletonList(keyContext)).build(); + if (isMapLoader()) { + singleLoadCall = invokeMapBatchLoader(keys, environment).thenApply(list -> list.get(0)); + } else { + singleLoadCall = invokeListBatchLoader(keys, environment).thenApply(list -> list.get(0)); + } + return singleLoadCall.toCompletableFuture(); + } catch (Exception e) { + return CompletableFutureKit.failedFuture(e); + } + } + + CompletionStage> invokeLoader(List keys, List keyContexts) { + CompletionStage> batchLoad; + try { + Object context = loaderOptions.getBatchLoaderContextProvider().getContext(); + BatchLoaderEnvironment environment = BatchLoaderEnvironment.newBatchLoaderEnvironment() + .context(context).keyContexts(keys, keyContexts).build(); + if (isMapLoader()) { + batchLoad = invokeMapBatchLoader(keys, environment); + } else { + batchLoad = invokeListBatchLoader(keys, environment); + } + } catch (Exception e) { + batchLoad = CompletableFutureKit.failedFuture(e); + } + return batchLoad; + } + + @SuppressWarnings("unchecked") + private CompletionStage> invokeListBatchLoader(List keys, BatchLoaderEnvironment environment) { + CompletionStage> loadResult; + if (batchLoadFunction instanceof BatchLoaderWithContext) { + loadResult = ((BatchLoaderWithContext) batchLoadFunction).load(keys, environment); + } else { + loadResult = ((BatchLoader) batchLoadFunction).load(keys); + } + 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. + */ + @SuppressWarnings("unchecked") + private CompletionStage> invokeMapBatchLoader(List keys, BatchLoaderEnvironment environment) { + CompletionStage> loadResult; + Set setOfKeys = new LinkedHashSet<>(keys); + if (batchLoadFunction instanceof MappedBatchLoaderWithContext) { + loadResult = ((MappedBatchLoaderWithContext) batchLoadFunction).load(setOfKeys, environment); + } else { + loadResult = ((MappedBatchLoader) batchLoadFunction).load(setOfKeys); + } + CompletionStage> mapBatchLoad = nonNull(loadResult, "Your batch loader function MUST return a non null CompletionStage promise"); + return mapBatchLoad.thenApply(map -> { + List values = new ArrayList<>(); + for (K key : keys) { + V value = map.get(key); + values.add(value); + } + return values; + }); + } + + private boolean isMapLoader() { + return batchLoadFunction instanceof MappedBatchLoader || batchLoadFunction instanceof MappedBatchLoaderWithContext; + } + + 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/org/dataloader/DataLoaderCacheTest.java b/src/test/java/org/dataloader/DataLoaderCacheTest.java new file mode 100644 index 0000000..bc52e5b --- /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(); + + 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 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(); + + 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(); + + 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(); + + 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_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/DataLoaderTest.java b/src/test/java/org/dataloader/DataLoaderTest.java index 9d18f67..180f011 100644 --- a/src/test/java/org/dataloader/DataLoaderTest.java +++ b/src/test/java/org/dataloader/DataLoaderTest.java @@ -37,13 +37,15 @@ 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; /** @@ -111,7 +113,7 @@ public void should_Return_zero_entries_dispatched_when_no_keys_supplied() { assertThat(promisedValues.size(), is(0)); success.set(true); }); - DispatchResult dispatchResult = identityLoader.dispatchWithCounts(); + DispatchResult dispatchResult = identityLoader.dispatchWithCounts(); await().untilAtomic(success, is(true)); assertThat(dispatchResult.getKeysCount(), equalTo(0)); } @@ -162,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 { @@ -214,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 { @@ -473,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() { @@ -730,77 +486,6 @@ 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() { @@ -1020,32 +705,6 @@ public void should_allow_composition_of_data_loader_calls() { } - 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 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); - } - - private static DataLoader idLoaderBlowsUps( - DataLoaderOptions options, List> loadCalls) { - return newDataLoader(keys -> { - loadCalls.add(new ArrayList<>(keys)); - return TestKit.futureError(); - }, options); - } - private static DataLoader idLoaderAllExceptions( DataLoaderOptions options, List> loadCalls) { return newDataLoader(keys -> { 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")); } From 5ac6c289bb29aa78c410ca78d257ffca85606b16 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Wed, 25 Mar 2020 11:48:46 +1100 Subject: [PATCH 3/3] Collapsed the two dispatch methods into one --- src/main/java/org/dataloader/DataLoader.java | 13 ++-------- .../org/dataloader/DataLoaderRegistry.java | 26 +++++++------------ .../org/dataloader/impl/DataLoaderImpl.java | 11 +++----- .../org/dataloader/DataLoaderCacheTest.java | 18 ++++++------- .../DataLoaderMapBatchLoaderTest.java | 2 +- .../dataloader/DataLoaderRegistryTest.java | 2 +- .../java/org/dataloader/DataLoaderTest.java | 20 +++++++------- 7 files changed, 35 insertions(+), 57 deletions(-) diff --git a/src/main/java/org/dataloader/DataLoader.java b/src/main/java/org/dataloader/DataLoader.java index d59b6f3..44a3d79 100644 --- a/src/main/java/org/dataloader/DataLoader.java +++ b/src/main/java/org/dataloader/DataLoader.java @@ -127,16 +127,7 @@ public interface DataLoader { /** - * Dispatches the queued load requests to the batch execution function and returns a promise of the result. - *

- * If batching is disabled, or there are no queued requests, then a succeeded promise is returned. - * - * @return the promise of the queued load requests - */ - CompletableFuture> dispatch(); - - /** - * 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 @@ -144,7 +135,7 @@ public interface DataLoader { * * @return the promise of the queued load requests and the number of keys dispatched. */ - DispatchResult dispatchWithCounts(); + DispatchResult dispatch(); /** * Normally {@link #dispatch()} is an asynchronous operation but this version will 'join' on the diff --git a/src/main/java/org/dataloader/DataLoaderRegistry.java b/src/main/java/org/dataloader/DataLoaderRegistry.java index 3f32b0b..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,9 +11,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; -import org.dataloader.annotations.PublicApi; -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 @@ -105,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; } @@ -132,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/impl/DataLoaderImpl.java b/src/main/java/org/dataloader/impl/DataLoaderImpl.java index 6dfe0e5..e15ba9c 100644 --- a/src/main/java/org/dataloader/impl/DataLoaderImpl.java +++ b/src/main/java/org/dataloader/impl/DataLoaderImpl.java @@ -113,22 +113,17 @@ public CompletableFuture> loadMany(List keys, List keyContext } @Override - public CompletableFuture> dispatch() { - return dispatchImpl().getPromisedResults(); - } - - @Override - public DispatchResult dispatchWithCounts() { + public DispatchResult dispatch() { return dispatchImpl(); } @Override public List dispatchAndJoin() { - List joinedResults = dispatch().join(); + List joinedResults = dispatch().getPromisedResults().join(); List results = new ArrayList<>(joinedResults); while (this.dispatchDepth() > 0) { - joinedResults = dispatch().join(); + joinedResults = dispatch().getPromisedResults().join(); results.addAll(joinedResults); } return results; diff --git a/src/test/java/org/dataloader/DataLoaderCacheTest.java b/src/test/java/org/dataloader/DataLoaderCacheTest.java index bc52e5b..3c3da7b 100644 --- a/src/test/java/org/dataloader/DataLoaderCacheTest.java +++ b/src/test/java/org/dataloader/DataLoaderCacheTest.java @@ -71,7 +71,7 @@ public void should_Accept_a_custom_cache_map_implementation() throws ExecutionEx CompletableFuture future1 = identityLoader.load("a"); CompletableFuture future2 = identityLoader.load("b"); - CompletableFuture> composite = identityLoader.dispatch(); + CompletableFuture> composite = identityLoader.dispatch().getPromisedResults(); await().until(composite::isDone); assertThat(future1.get(), equalTo("a")); @@ -82,7 +82,7 @@ public void should_Accept_a_custom_cache_map_implementation() throws ExecutionEx CompletableFuture future3 = identityLoader.load("c"); CompletableFuture future2a = identityLoader.load("b"); - composite = identityLoader.dispatch(); + composite = identityLoader.dispatch().getPromisedResults(); await().until(composite::isDone); assertThat(future3.get(), equalTo("c")); @@ -97,7 +97,7 @@ public void should_Accept_a_custom_cache_map_implementation() throws ExecutionEx assertArrayEquals(customMap.stash.keySet().toArray(), asList("a", "c").toArray()); CompletableFuture future2b = identityLoader.load("b"); - composite = identityLoader.dispatch(); + composite = identityLoader.dispatch().getPromisedResults(); await().until(composite::isDone); assertThat(future2b.get(), equalTo("b")); @@ -122,7 +122,7 @@ public void should_allow_values_extracted_from_cache_on_load() { CompletableFuture future1 = identityLoader.load("a"); CompletableFuture future2 = identityLoader.load("b"); - CompletableFuture> composite = identityLoader.dispatch(); + CompletableFuture> composite = identityLoader.dispatch().getPromisedResults(); await().until(composite::isDone); assertThat(future1.join(), equalTo("cachedVal")); @@ -143,7 +143,7 @@ public void should_allow_promise_map_to_be_used() { CompletableFuture future1 = identityLoader.load("a"); CompletableFuture future2 = identityLoader.load("b"); - CompletableFuture> composite = identityLoader.dispatch(); + CompletableFuture> composite = identityLoader.dispatch().getPromisedResults(); await().until(composite::isDone); assertThat(future1.join(), equalTo("customValue")); @@ -248,7 +248,7 @@ public void should_Not_prime_keys_that_already_exist() throws ExecutionException CompletableFuture future1 = identityLoader.load("A"); CompletableFuture future2 = identityLoader.load("B"); - CompletableFuture> composite = identityLoader.dispatch(); + CompletableFuture> composite = identityLoader.dispatch().getPromisedResults(); await().until(composite::isDone); assertThat(future1.get(), equalTo("X")); @@ -259,7 +259,7 @@ public void should_Not_prime_keys_that_already_exist() throws ExecutionException CompletableFuture future1a = identityLoader.load("A"); CompletableFuture future2a = identityLoader.load("B"); - CompletableFuture> composite2 = identityLoader.dispatch(); + CompletableFuture> composite2 = identityLoader.dispatch().getPromisedResults(); await().until(composite2::isDone); assertThat(future1a.get(), equalTo("X")); @@ -276,7 +276,7 @@ public void should_Allow_to_forcefully_prime_the_cache() throws ExecutionExcepti CompletableFuture future1 = identityLoader.load("A"); CompletableFuture future2 = identityLoader.load("B"); - CompletableFuture> composite = identityLoader.dispatch(); + CompletableFuture> composite = identityLoader.dispatch().getPromisedResults(); await().until(composite::isDone); assertThat(future1.get(), equalTo("X")); @@ -287,7 +287,7 @@ public void should_Allow_to_forcefully_prime_the_cache() throws ExecutionExcepti CompletableFuture future1a = identityLoader.load("A"); CompletableFuture future2a = identityLoader.load("B"); - CompletableFuture> composite2 = identityLoader.dispatch(); + CompletableFuture> composite2 = identityLoader.dispatch().getPromisedResults(); await().until(composite2::isDone); assertThat(future1a.get(), equalTo("Y")); diff --git a/src/test/java/org/dataloader/DataLoaderMapBatchLoaderTest.java b/src/test/java/org/dataloader/DataLoaderMapBatchLoaderTest.java index ccafbe7..6c0557f 100644 --- a/src/test/java/org/dataloader/DataLoaderMapBatchLoaderTest.java +++ b/src/test/java/org/dataloader/DataLoaderMapBatchLoaderTest.java @@ -110,7 +110,7 @@ public void can_split_max_batch_sizes_correctly() { 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 e941d9f..4151756 100644 --- a/src/test/java/org/dataloader/DataLoaderRegistryTest.java +++ b/src/test/java/org/dataloader/DataLoaderRegistryTest.java @@ -152,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/DataLoaderTest.java b/src/test/java/org/dataloader/DataLoaderTest.java index 180f011..6efbcdc 100644 --- a/src/test/java/org/dataloader/DataLoaderTest.java +++ b/src/test/java/org/dataloader/DataLoaderTest.java @@ -113,7 +113,7 @@ public void should_Return_zero_entries_dispatched_when_no_keys_supplied() { 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)); } @@ -141,7 +141,7 @@ public void should_Return_number_of_batched_entries() { 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 @@ -215,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)); @@ -316,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)); }); @@ -334,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)); }); @@ -582,7 +582,7 @@ public void can_split_max_batch_sizes_correctly() { 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)); @@ -647,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"));