diff --git a/README.md b/README.md index 4d19bad..293d0ba 100644 --- a/README.md +++ b/README.md @@ -269,6 +269,38 @@ In some circumstances you may wish to clear the cache for these individual probl }); ``` + +## Statistics on what is happening + +`DataLoader` keeps statistics on what is happening. It can tell you the number of objects asked for, the cache hit number, the number of objects +asked for via batching and so on. + +Knowing what the behaviour of your data is important for you to understand how efficient you are in serving the data via this pattern. + + +```java + Statistics statistics = userDataLoader.getStatistics(); + + System.out.println(format("load : %d", statistics.getLoadCount())); + System.out.println(format("batch load: %d", statistics.getBatchLoadCount())); + System.out.println(format("cache hit: %d", statistics.getCacheHitCount())); + System.out.println(format("cache hit ratio: %d", statistics.getCacheHitRatio())); + +``` + +`DataLoaderRegistry` can also roll up the statistics for all data loaders inside it. + +You can configure the statistics collector used when you build the data loader + +```java + DataLoaderOptions options = DataLoaderOptions.newOptions().setStatisticsCollector(() -> new ThreadLocalStatisticsCollector()); + DataLoader userDataLoader = DataLoader.newDataLoader(userBatchLoader,options); + +``` + +Which collector you use is up to you. It ships with the following: `SimpleStatisticsCollector`, `ThreadLocalStatisticsCollector`, `DelegatingStatisticsCollector` +and `NoOpStatisticsCollector`. + ## The scope of a data loader is important If you are serving web requests then the data can be specific to the user requesting it. If you have user specific data diff --git a/src/main/java/org/dataloader/DataLoader.java b/src/main/java/org/dataloader/DataLoader.java index 056b8b4..a3fdc2b 100644 --- a/src/main/java/org/dataloader/DataLoader.java +++ b/src/main/java/org/dataloader/DataLoader.java @@ -17,6 +17,8 @@ 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.Collection; @@ -24,6 +26,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; import java.util.stream.Collectors; import static java.util.Collections.emptyList; @@ -64,6 +67,7 @@ public class DataLoader { private final DataLoaderOptions loaderOptions; private final CacheMap> futureCache; private final Map> loaderQueue; + private final StatisticsCollector stats; /** * Creates new DataLoader with the specified batch loader function and default options @@ -153,6 +157,7 @@ public DataLoader(BatchLoader batchLoadFunction, DataLoaderOptions options this.futureCache = determineCacheMap(loaderOptions); // order of keys matter in data loader this.loaderQueue = new LinkedHashMap<>(); + this.stats = nonNull(this.loaderOptions.getStatisticsCollector()); } @SuppressWarnings("unchecked") @@ -173,8 +178,11 @@ private CacheMap> determineCacheMap(DataLoaderOptio */ public CompletableFuture load(K key) { Object cacheKey = getCacheKey(nonNull(key)); + stats.incrementLoadCount(); + synchronized (futureCache) { if (loaderOptions.cachingEnabled() && futureCache.containsKey(cacheKey)) { + stats.incrementCacheHitCount(); return futureCache.get(cacheKey); } } @@ -185,6 +193,7 @@ public CompletableFuture load(K key) { loaderQueue.put(key, future); } } else { + stats.incrementBatchLoadCountBy(1); // immediate execution of batch function CompletableFuture> batchedLoad = batchLoadFunction .load(singletonList(key)) @@ -291,7 +300,14 @@ private CompletableFuture> sliceIntoBatchesOfBatches(List keys, List< @SuppressWarnings("unchecked") private CompletableFuture> dispatchQueueBatch(List keys, List> queuedFutures) { - return batchLoadFunction.load(keys) + stats.incrementBatchLoadCountBy(keys.size()); + CompletionStage> batchLoad; + try { + batchLoad = nonNull(batchLoadFunction.load(keys), "Your batch loader function MUST return a non null CompletionStage promise"); + } catch (Exception e) { + batchLoad = CompletableFutureKit.failedFuture(e); + } + return batchLoad .toCompletableFuture() .thenApply(values -> { assertState(keys.size() == values.size(), "The size of the promised values MUST be the same size as the key list"); @@ -300,13 +316,20 @@ private CompletableFuture> dispatchQueueBatch(List keys, List future = queuedFutures.get(idx); if (value instanceof Throwable) { + stats.incrementLoadErrorCount(); future.completeExceptionally((Throwable) value); // we don't clear the cached view of this entry to avoid // frequently loading the same error } 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. - handleTry((Try) value, future); + Try tryValue = (Try) value; + if (tryValue.isSuccess()) { + future.complete(tryValue.get()); + } else { + stats.incrementLoadErrorCount(); + future.completeExceptionally(tryValue.getThrowable()); + } } else { V val = (V) value; future.complete(val); @@ -314,6 +337,7 @@ private CompletableFuture> dispatchQueueBatch(List keys, List { + stats.incrementBatchLoadExceptionCount(); for (int idx = 0; idx < queuedFutures.size(); idx++) { K key = keys.get(idx); CompletableFuture future = queuedFutures.get(idx); @@ -325,14 +349,6 @@ private CompletableFuture> dispatchQueueBatch(List keys, List vTry, CompletableFuture future) { - if (vTry.isSuccess()) { - future.complete(vTry.get()); - } else { - future.completeExceptionally(vTry.getThrowable()); - } - } - /** * 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 @@ -441,4 +457,15 @@ public Object getCacheKey(K key) { return loaderOptions.cacheKeyFunction().isPresent() ? loaderOptions.cacheKeyFunction().get().getKey(key) : 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(); + } + } diff --git a/src/main/java/org/dataloader/DataLoaderOptions.java b/src/main/java/org/dataloader/DataLoaderOptions.java index 02d10ff..97b19ad 100644 --- a/src/main/java/org/dataloader/DataLoaderOptions.java +++ b/src/main/java/org/dataloader/DataLoaderOptions.java @@ -16,9 +16,13 @@ package org.dataloader; -import org.dataloader.impl.Assertions; +import org.dataloader.stats.SimpleStatisticsCollector; +import org.dataloader.stats.StatisticsCollector; import java.util.Optional; +import java.util.function.Supplier; + +import static org.dataloader.impl.Assertions.nonNull; /** * Configuration options for {@link DataLoader} instances. @@ -32,6 +36,7 @@ public class DataLoaderOptions { private CacheKey cacheKeyFunction; private CacheMap cacheMap; private int maxBatchSize; + private Supplier statisticsCollector; /** * Creates a new data loader options with default settings. @@ -40,6 +45,7 @@ public DataLoaderOptions() { batchingEnabled = true; cachingEnabled = true; maxBatchSize = -1; + statisticsCollector = SimpleStatisticsCollector::new; } /** @@ -48,12 +54,13 @@ public DataLoaderOptions() { * @param other the other options instance */ public DataLoaderOptions(DataLoaderOptions other) { - Assertions.nonNull(other); + nonNull(other); this.batchingEnabled = other.batchingEnabled; this.cachingEnabled = other.cachingEnabled; this.cacheKeyFunction = other.cacheKeyFunction; this.cacheMap = other.cacheMap; this.maxBatchSize = other.maxBatchSize; + this.statisticsCollector = other.statisticsCollector; } /** @@ -173,4 +180,27 @@ public DataLoaderOptions setMaxBatchSize(int maxBatchSize) { this.maxBatchSize = maxBatchSize; return this; } + + /** + * @return the statistics collector to use with these options + */ + public StatisticsCollector getStatisticsCollector() { + return nonNull(this.statisticsCollector.get()); + } + + /** + * Sets the statistics collector supplier that will be used with these data loader options. Since it uses + * the supplier pattern, you can create a new statistics collector on each call or you can reuse + * a common value + * + * @param statisticsCollector the statistics collector to use + * + * @return the data loader options for fluent coding + */ + public DataLoaderOptions setStatisticsCollector(Supplier statisticsCollector) { + this.statisticsCollector = nonNull(statisticsCollector); + return this; + } + + } diff --git a/src/main/java/org/dataloader/DataLoaderRegistry.java b/src/main/java/org/dataloader/DataLoaderRegistry.java index 2c11d4c..f75ddc4 100644 --- a/src/main/java/org/dataloader/DataLoaderRegistry.java +++ b/src/main/java/org/dataloader/DataLoaderRegistry.java @@ -1,5 +1,7 @@ package org.dataloader; +import org.dataloader.stats.Statistics; + import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -91,4 +93,16 @@ public Set getKeys() { public void dispatchAll() { getDataLoaders().forEach(DataLoader::dispatch); } + + /** + * @return a combined set of statistics for all data loaders in this registry presented + * as the sum of all their statistics + */ + public Statistics getStatistics() { + Statistics stats = new Statistics(); + for (DataLoader dataLoader : dataLoaders.values()) { + stats = stats.combine(dataLoader.getStatistics()); + } + return stats; + } } diff --git a/src/main/java/org/dataloader/impl/Assertions.java b/src/main/java/org/dataloader/impl/Assertions.java index 86dac33..ad1a1ef 100644 --- a/src/main/java/org/dataloader/impl/Assertions.java +++ b/src/main/java/org/dataloader/impl/Assertions.java @@ -14,6 +14,10 @@ public static T nonNull(T t) { return Objects.requireNonNull(t, "nonNull object required"); } + public static T nonNull(T t, String message) { + return Objects.requireNonNull(t, message); + } + private static class AssertionException extends IllegalStateException { public AssertionException(String message) { super(message); diff --git a/src/main/java/org/dataloader/stats/DelegatingStatisticsCollector.java b/src/main/java/org/dataloader/stats/DelegatingStatisticsCollector.java new file mode 100644 index 0000000..f44c521 --- /dev/null +++ b/src/main/java/org/dataloader/stats/DelegatingStatisticsCollector.java @@ -0,0 +1,67 @@ +package org.dataloader.stats; + +import static org.dataloader.impl.Assertions.nonNull; + +/** + * This statistics collector keeps dataloader statistics AND also calls the delegate + * collector at the same time. This allows you to keep a specific set of statistics + * and also delegate the calls onto another collector. + */ +public class DelegatingStatisticsCollector implements StatisticsCollector { + + private final StatisticsCollector collector = new SimpleStatisticsCollector(); + private final StatisticsCollector delegateCollector; + + /** + * @param delegateCollector a non null delegate collector + */ + public DelegatingStatisticsCollector(StatisticsCollector delegateCollector) { + this.delegateCollector = nonNull(delegateCollector); + } + + @Override + public long incrementLoadCount() { + delegateCollector.incrementLoadCount(); + return collector.incrementLoadCount(); + } + + @Override + public long incrementBatchLoadCountBy(long delta) { + delegateCollector.incrementBatchLoadCountBy(delta); + return collector.incrementBatchLoadCountBy(delta); + } + + @Override + public long incrementCacheHitCount() { + delegateCollector.incrementCacheHitCount(); + return collector.incrementCacheHitCount(); + } + + @Override + public long incrementLoadErrorCount() { + delegateCollector.incrementLoadErrorCount(); + return collector.incrementLoadErrorCount(); + } + + @Override + public long incrementBatchLoadExceptionCount() { + delegateCollector.incrementBatchLoadExceptionCount(); + return collector.incrementBatchLoadExceptionCount(); + } + + /** + * @return the statistics of the this collector (and not its delegate) + */ + @Override + public Statistics getStatistics() { + return collector.getStatistics(); + } + + /** + * @return the statistics of the delegate + */ + public Statistics getDelegateStatistics() { + return delegateCollector.getStatistics(); + } + +} diff --git a/src/main/java/org/dataloader/stats/NoOpStatisticsCollector.java b/src/main/java/org/dataloader/stats/NoOpStatisticsCollector.java new file mode 100644 index 0000000..3c3624f --- /dev/null +++ b/src/main/java/org/dataloader/stats/NoOpStatisticsCollector.java @@ -0,0 +1,39 @@ +package org.dataloader.stats; + +/** + * A statistics collector that does nothing + */ +public class NoOpStatisticsCollector implements StatisticsCollector { + + private static final Statistics ZERO_STATS = new Statistics(); + + @Override + public long incrementLoadCount() { + return 0; + } + + @Override + public long incrementLoadErrorCount() { + return 0; + } + + @Override + public long incrementBatchLoadCountBy(long delta) { + return 0; + } + + @Override + public long incrementBatchLoadExceptionCount() { + return 0; + } + + @Override + public long incrementCacheHitCount() { + return 0; + } + + @Override + public Statistics getStatistics() { + return ZERO_STATS; + } +} diff --git a/src/main/java/org/dataloader/stats/SimpleStatisticsCollector.java b/src/main/java/org/dataloader/stats/SimpleStatisticsCollector.java new file mode 100644 index 0000000..af48b0c --- /dev/null +++ b/src/main/java/org/dataloader/stats/SimpleStatisticsCollector.java @@ -0,0 +1,55 @@ +package org.dataloader.stats; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * This simple collector uses {@link java.util.concurrent.atomic.AtomicLong}s to collect + * statistics + * + * @see org.dataloader.stats.StatisticsCollector + */ +public class SimpleStatisticsCollector implements StatisticsCollector { + private final AtomicLong loadCount = new AtomicLong(); + private final AtomicLong batchInvokeCount = new AtomicLong(); + private final AtomicLong batchLoadCount = new AtomicLong(); + private final AtomicLong cacheHitCount = new AtomicLong(); + private final AtomicLong batchLoadExceptionCount = new AtomicLong(); + private final AtomicLong loadErrorCount = new AtomicLong(); + + @Override + public long incrementLoadCount() { + return loadCount.incrementAndGet(); + } + + + @Override + public long incrementBatchLoadCountBy(long delta) { + batchInvokeCount.incrementAndGet(); + return batchLoadCount.addAndGet(delta); + } + + @Override + public long incrementCacheHitCount() { + return cacheHitCount.incrementAndGet(); + } + + @Override + public long incrementLoadErrorCount() { + return loadErrorCount.incrementAndGet(); + } + + @Override + public long incrementBatchLoadExceptionCount() { + return batchLoadExceptionCount.incrementAndGet(); + } + + @Override + public Statistics getStatistics() { + return new Statistics(loadCount.get(), loadErrorCount.get(), batchInvokeCount.get(), batchLoadCount.get(), batchLoadExceptionCount.get(), cacheHitCount.get()); + } + + @Override + public String toString() { + return getStatistics().toString(); + } +} diff --git a/src/main/java/org/dataloader/stats/Statistics.java b/src/main/java/org/dataloader/stats/Statistics.java new file mode 100644 index 0000000..bf8da1a --- /dev/null +++ b/src/main/java/org/dataloader/stats/Statistics.java @@ -0,0 +1,172 @@ +package org.dataloader.stats; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * This holds statistics on how a {@link org.dataloader.DataLoader} has performed + */ +public class Statistics { + + private final long loadCount; + private final long loadErrorCount; + private final long batchInvokeCount; + private final long batchLoadCount; + private final long batchLoadExceptionCount; + private final long cacheHitCount; + + /** + * Zero statistics + */ + public Statistics() { + this(0, 0, 0, 0, 0, 0); + } + + public Statistics(long loadCount, long loadErrorCount, long batchInvokeCount, long batchLoadCount, long batchLoadExceptionCount, long cacheHitCount) { + this.loadCount = loadCount; + this.batchInvokeCount = batchInvokeCount; + this.batchLoadCount = batchLoadCount; + this.cacheHitCount = cacheHitCount; + this.batchLoadExceptionCount = batchLoadExceptionCount; + this.loadErrorCount = loadErrorCount; + } + + /** + * A helper to divide two numbers and handle zero + * + * @param numerator the top bit + * @param denominator the bottom bit + * + * @return numerator / denominator returning zero when denominator is zero + */ + public double ratio(long numerator, long denominator) { + return denominator == 0 ? 0f : ((double) numerator) / ((double) denominator); + } + + /** + * @return the number of objects {@link org.dataloader.DataLoader#load(Object)} has been asked to load + */ + public long getLoadCount() { + return loadCount; + } + + /** + * @return the number of times the {@link org.dataloader.DataLoader} batch loader function return an specific object that was in error + */ + public long getLoadErrorCount() { + return loadErrorCount; + } + + /** + * @return loadErrorCount / loadCount + */ + public double getLoadErrorRatio() { + return ratio(loadErrorCount, loadCount); + } + + /** + * @return the number of times the {@link org.dataloader.DataLoader} batch loader function has been called + */ + public long getBatchInvokeCount() { + return batchInvokeCount; + } + + /** + * @return the number of objects that the {@link org.dataloader.DataLoader} batch loader function has been asked to load + */ + public long getBatchLoadCount() { + return batchLoadCount; + } + + /** + * @return batchLoadCount / loadCount + */ + public double getBatchLoadRatio() { + return ratio(batchLoadCount, loadCount); + } + + /** + * @return the number of times the {@link org.dataloader.DataLoader} batch loader function throw an exception when trying to get any values + */ + public long getBatchLoadExceptionCount() { + return batchLoadExceptionCount; + } + + /** + * @return batchLoadExceptionCount / loadCount + */ + public double getBatchLoadExceptionRatio() { + return ratio(batchLoadExceptionCount, loadCount); + } + + /** + * @return the number of times {@link org.dataloader.DataLoader#load(Object)} resulted in a cache hit + */ + public long getCacheHitCount() { + return cacheHitCount; + } + + /** + * @return then number of times we missed the cache during {@link org.dataloader.DataLoader#load(Object)} + */ + public long getCacheMissCount() { + return loadCount - cacheHitCount; + } + + /** + * @return cacheHits / loadCount + */ + public double getCacheHitRatio() { + return ratio(cacheHitCount, loadCount); + } + + + /** + * This will combine this set of statistics with another set of statistics so that they become the combined count of each + * + * @param other the other statistics to combine + * + * @return a new statistics object of the combined counts + */ + public Statistics combine(Statistics other) { + return new Statistics( + this.loadCount + other.getLoadCount(), + this.loadErrorCount + other.getLoadErrorCount(), + this.batchInvokeCount + other.getBatchInvokeCount(), + this.batchLoadCount + other.getBatchLoadCount(), + this.batchLoadExceptionCount + other.getBatchLoadExceptionCount(), + this.cacheHitCount + other.getCacheHitCount() + ); + } + + /** + * @return a map representation of the statistics, perhaps to send over JSON or some such + */ + public Map toMap() { + Map stats = new LinkedHashMap<>(); + stats.put("loadCount", getLoadCount()); + stats.put("loadErrorCount", getLoadErrorCount()); + stats.put("loadErrorRatio", getLoadErrorRatio()); + + stats.put("batchInvokeCount", getBatchInvokeCount()); + stats.put("batchLoadCount", getBatchLoadCount()); + stats.put("batchLoadRatio", getBatchLoadRatio()); + stats.put("batchLoadExceptionCount", getBatchLoadExceptionCount()); + stats.put("batchLoadExceptionRatio", getBatchLoadExceptionRatio()); + + stats.put("cacheHitCount", getCacheHitCount()); + stats.put("cacheHitRatio", getCacheHitRatio()); + return stats; + } + + @Override + public String toString() { + return "Statistics{" + + "loadCount=" + loadCount + + ", loadErrorCount=" + loadErrorCount + + ", batchLoadCount=" + batchLoadCount + + ", batchLoadExceptionCount=" + batchLoadExceptionCount + + ", cacheHitCount=" + cacheHitCount + + '}'; + } +} diff --git a/src/main/java/org/dataloader/stats/StatisticsCollector.java b/src/main/java/org/dataloader/stats/StatisticsCollector.java new file mode 100644 index 0000000..874e27b --- /dev/null +++ b/src/main/java/org/dataloader/stats/StatisticsCollector.java @@ -0,0 +1,49 @@ +package org.dataloader.stats; + +/** + * This allows statistics to be collected for {@link org.dataloader.DataLoader} operations + */ +public interface StatisticsCollector { + + /** + * Called to increment the number of loads + * + * @return the current value after increment + */ + long incrementLoadCount(); + + /** + * Called to increment the number of loads that resulted in an object deemed in error + * + * @return the current value after increment + */ + long incrementLoadErrorCount(); + + /** + * Called to increment the number of batch loads + * + * @param delta how much to add to the count + * + * @return the current value after increment + */ + long incrementBatchLoadCountBy(long delta); + + /** + * Called to increment the number of batch loads exceptions + * + * @return the current value after increment + */ + long incrementBatchLoadExceptionCount(); + + /** + * Called to increment the number of cache hits + * + * @return the current value after increment + */ + long incrementCacheHitCount(); + + /** + * @return the statistics that have been gathered up to this point in time + */ + Statistics getStatistics(); +} diff --git a/src/main/java/org/dataloader/stats/ThreadLocalStatisticsCollector.java b/src/main/java/org/dataloader/stats/ThreadLocalStatisticsCollector.java new file mode 100644 index 0000000..7093584 --- /dev/null +++ b/src/main/java/org/dataloader/stats/ThreadLocalStatisticsCollector.java @@ -0,0 +1,87 @@ +package org.dataloader.stats; + +/** + * This can collect statistics per thread as well as in an overall sense. This allows you to snapshot stats for a web request say + * as well as all requests. + * + * You will want to call {@link #resetThread()} to clean up the thread local aspects of this object per request thread. + * + * ThreadLocals have their place in the Java world but be careful on how you use them. If you don't clean them up on "request boundaries" + * then you WILL have misleading statistics. + * + * @see org.dataloader.stats.StatisticsCollector + */ +public class ThreadLocalStatisticsCollector implements StatisticsCollector { + + private static final ThreadLocal collector = ThreadLocal.withInitial(SimpleStatisticsCollector::new); + + private final SimpleStatisticsCollector overallCollector = new SimpleStatisticsCollector(); + + /** + * Removes the underlying thread local value for this current thread. This is a way to reset the thread local + * values for the current thread and start afresh + * + * @return this collector for fluent coding + */ + public ThreadLocalStatisticsCollector resetThread() { + collector.remove(); + return this; + } + + @Override + public long incrementLoadCount() { + overallCollector.incrementLoadCount(); + return collector.get().incrementLoadCount(); + } + + @Override + public long incrementBatchLoadCountBy(long delta) { + overallCollector.incrementBatchLoadCountBy(delta); + return collector.get().incrementBatchLoadCountBy(delta); + } + + @Override + public long incrementCacheHitCount() { + overallCollector.incrementCacheHitCount(); + return collector.get().incrementCacheHitCount(); + } + + @Override + public long incrementLoadErrorCount() { + overallCollector.incrementLoadErrorCount(); + return collector.get().incrementLoadErrorCount(); + } + + @Override + public long incrementBatchLoadExceptionCount() { + overallCollector.incrementBatchLoadExceptionCount(); + return collector.get().incrementBatchLoadExceptionCount(); + } + + /** + * This returns the statistics for this thread. + * + * @return this thread's statistics + */ + @Override + public Statistics getStatistics() { + return collector.get().getStatistics(); + } + + /** + * This returns the overall statistics, that is not per thread but for the life of this object + * + * @return overall statistics + */ + public Statistics getOverallStatistics() { + return overallCollector.getStatistics(); + } + + @Override + public String toString() { + return "ThreadLocalStatisticsCollector{" + + "thread=" + getStatistics().toString() + + "overallCollector=" + overallCollector.getStatistics().toString() + + '}'; + } +} diff --git a/src/test/java/ReadmeExamples.java b/src/test/java/ReadmeExamples.java index fe94f23..1fa722e 100644 --- a/src/test/java/ReadmeExamples.java +++ b/src/test/java/ReadmeExamples.java @@ -5,6 +5,8 @@ import org.dataloader.Try; import org.dataloader.fixtures.User; import org.dataloader.fixtures.UserManager; +import org.dataloader.stats.Statistics; +import org.dataloader.stats.ThreadLocalStatisticsCollector; import java.util.ArrayList; import java.util.List; @@ -12,6 +14,8 @@ import java.util.concurrent.CompletionStage; import java.util.stream.Collectors; +import static java.lang.String.format; + @SuppressWarnings("ALL") public class ReadmeExamples { @@ -75,7 +79,6 @@ public CompletionStage> load(List userIds) { } - private void tryExample() { Try tryS = Try.tryCall(() -> { if (rollDice()) { @@ -185,4 +188,19 @@ private boolean rollDice() { } + private void statsExample() { + Statistics statistics = userDataLoader.getStatistics(); + + System.out.println(format("load : %d", statistics.getLoadCount())); + System.out.println(format("batch load: %d", statistics.getBatchLoadCount())); + System.out.println(format("cache hit: %d", statistics.getCacheHitCount())); + System.out.println(format("cache hit ratio: %d", statistics.getCacheHitRatio())); + } + + private void statsConfigExample() { + + DataLoaderOptions options = DataLoaderOptions.newOptions().setStatisticsCollector(() -> new ThreadLocalStatisticsCollector()); + DataLoader userDataLoader = DataLoader.newDataLoader(userBatchLoader,options); + } + } diff --git a/src/test/java/org/dataloader/DataLoaderRegistryTest.java b/src/test/java/org/dataloader/DataLoaderRegistryTest.java index 1e1c035..aa6a84f 100644 --- a/src/test/java/org/dataloader/DataLoaderRegistryTest.java +++ b/src/test/java/org/dataloader/DataLoaderRegistryTest.java @@ -1,5 +1,6 @@ package org.dataloader; +import org.dataloader.stats.Statistics; import org.junit.Test; import java.util.concurrent.CompletableFuture; @@ -68,4 +69,36 @@ public void registries_can_be_combined() throws Exception { assertThat(combinedRegistry.getKeys(), hasItems("a", "b", "c", "d")); assertThat(combinedRegistry.getDataLoaders(), hasItems(dlA, dlB, dlC, dlD)); } + + @Test + public void stats_can_be_collected() throws Exception { + + DataLoaderRegistry registry = new DataLoaderRegistry(); + + DataLoader dlA = new DataLoader<>(identityBatchLoader); + DataLoader dlB = new DataLoader<>(identityBatchLoader); + DataLoader dlC = new DataLoader<>(identityBatchLoader); + + registry.register("a", dlA).register("b", dlB).register("c", dlC); + + dlA.load("X"); + dlB.load("Y"); + dlC.load("Z"); + + registry.dispatchAll(); + + dlA.load("X"); + dlB.load("Y"); + dlC.load("Z"); + + registry.dispatchAll(); + + Statistics statistics = registry.getStatistics(); + + assertThat(statistics.getLoadCount(), equalTo(6L)); + assertThat(statistics.getBatchLoadCount(), equalTo(3L)); + assertThat(statistics.getCacheHitCount(), equalTo(3L)); + assertThat(statistics.getLoadErrorCount(), equalTo(0L)); + assertThat(statistics.getBatchLoadExceptionCount(), equalTo(0L)); + } } \ No newline at end of file diff --git a/src/test/java/org/dataloader/DataLoaderStatsTest.java b/src/test/java/org/dataloader/DataLoaderStatsTest.java new file mode 100644 index 0000000..c6a355b --- /dev/null +++ b/src/test/java/org/dataloader/DataLoaderStatsTest.java @@ -0,0 +1,197 @@ +package org.dataloader; + +import org.dataloader.impl.CompletableFutureKit; +import org.dataloader.stats.SimpleStatisticsCollector; +import org.dataloader.stats.Statistics; +import org.dataloader.stats.StatisticsCollector; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import static java.util.Arrays.asList; +import static java.util.concurrent.CompletableFuture.completedFuture; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; + +/** + * Tests related to stats. DataLoaderTest is getting to big and needs refactoring + */ +public class DataLoaderStatsTest { + + @Test + public void stats_are_collected_by_default() throws Exception { + BatchLoader batchLoader = CompletableFuture::completedFuture; + DataLoader loader = new DataLoader<>(batchLoader); + + loader.load("A"); + loader.load("B"); + loader.loadMany(asList("C", "D")); + + Statistics stats = loader.getStatistics(); + assertThat(stats.getLoadCount(), equalTo(4L)); + assertThat(stats.getBatchInvokeCount(), equalTo(0L)); + assertThat(stats.getBatchLoadCount(), equalTo(0L)); + assertThat(stats.getCacheHitCount(), equalTo(0L)); + + loader.dispatch(); + + stats = loader.getStatistics(); + assertThat(stats.getLoadCount(), equalTo(4L)); + assertThat(stats.getBatchInvokeCount(), equalTo(1L)); + assertThat(stats.getBatchLoadCount(), equalTo(4L)); + assertThat(stats.getCacheHitCount(), equalTo(0L)); + + loader.load("A"); + loader.load("B"); + + loader.dispatch(); + + stats = loader.getStatistics(); + assertThat(stats.getLoadCount(), equalTo(6L)); + assertThat(stats.getBatchInvokeCount(), equalTo(1L)); + assertThat(stats.getBatchLoadCount(), equalTo(4L)); + assertThat(stats.getCacheHitCount(), equalTo(2L)); + } + + + @Test + public void stats_are_collected_with_specified_collector() throws Exception { + // lets prime it with some numbers so we know its ours + StatisticsCollector collector = new SimpleStatisticsCollector(); + collector.incrementLoadCount(); + collector.incrementBatchLoadCountBy(1); + + BatchLoader batchLoader = CompletableFuture::completedFuture; + DataLoaderOptions loaderOptions = DataLoaderOptions.newOptions().setStatisticsCollector(() -> collector); + DataLoader loader = new DataLoader<>(batchLoader, loaderOptions); + + loader.load("A"); + loader.load("B"); + loader.loadMany(asList("C", "D")); + + Statistics stats = loader.getStatistics(); + assertThat(stats.getLoadCount(), equalTo(5L)); // previously primed with 1 + assertThat(stats.getBatchInvokeCount(), equalTo(1L)); // also primed + assertThat(stats.getBatchLoadCount(), equalTo(1L)); // also primed + assertThat(stats.getCacheHitCount(), equalTo(0L)); + + loader.dispatch(); + + stats = loader.getStatistics(); + assertThat(stats.getLoadCount(), equalTo(5L)); + assertThat(stats.getBatchInvokeCount(), equalTo(2L)); + assertThat(stats.getBatchLoadCount(), equalTo(5L)); + assertThat(stats.getCacheHitCount(), equalTo(0L)); + + loader.load("A"); + loader.load("B"); + + loader.dispatch(); + + stats = loader.getStatistics(); + assertThat(stats.getLoadCount(), equalTo(7L)); + assertThat(stats.getBatchInvokeCount(), equalTo(2L)); + assertThat(stats.getBatchLoadCount(), equalTo(5L)); + assertThat(stats.getCacheHitCount(), equalTo(2L)); + } + + @Test + public void stats_are_collected_with_caching_disabled() throws Exception { + StatisticsCollector collector = new SimpleStatisticsCollector(); + + BatchLoader batchLoader = CompletableFuture::completedFuture; + DataLoaderOptions loaderOptions = DataLoaderOptions.newOptions().setStatisticsCollector(() -> collector).setCachingEnabled(false); + DataLoader loader = new DataLoader<>(batchLoader, loaderOptions); + + loader.load("A"); + loader.load("B"); + loader.loadMany(asList("C", "D")); + + Statistics stats = loader.getStatistics(); + assertThat(stats.getLoadCount(), equalTo(4L)); + assertThat(stats.getBatchInvokeCount(), equalTo(0L)); + assertThat(stats.getBatchLoadCount(), equalTo(0L)); + assertThat(stats.getCacheHitCount(), equalTo(0L)); + + loader.dispatch(); + + stats = loader.getStatistics(); + assertThat(stats.getLoadCount(), equalTo(4L)); + assertThat(stats.getBatchInvokeCount(), equalTo(1L)); + assertThat(stats.getBatchLoadCount(), equalTo(4L)); + assertThat(stats.getCacheHitCount(), equalTo(0L)); + + loader.load("A"); + loader.load("B"); + + loader.dispatch(); + + stats = loader.getStatistics(); + assertThat(stats.getLoadCount(), equalTo(6L)); + assertThat(stats.getBatchInvokeCount(), equalTo(2L)); + assertThat(stats.getBatchLoadCount(), equalTo(6L)); + assertThat(stats.getCacheHitCount(), equalTo(0L)); + } + + BatchLoader> batchLoaderThatBlows = keys -> { + List> values = new ArrayList<>(); + for (String key : keys) { + if (key.startsWith("exception")) { + return CompletableFutureKit.failedFuture(new RuntimeException(key)); + } else if (key.startsWith("bang")) { + throw new RuntimeException(key); + } else if (key.startsWith("error")) { + values.add(Try.failed(new RuntimeException(key))); + } else { + values.add(Try.succeeded(key)); + } + } + return completedFuture(values); + }; + + @Test + public void stats_are_collected_on_exceptions() throws Exception { + DataLoader loader = DataLoader.newDataLoaderWithTry(batchLoaderThatBlows); + + loader.load("A"); + loader.load("exception"); + + Statistics stats = loader.getStatistics(); + assertThat(stats.getLoadCount(), equalTo(2L)); + assertThat(stats.getBatchLoadExceptionCount(), equalTo(0L)); + assertThat(stats.getLoadErrorCount(), equalTo(0L)); + + loader.dispatch(); + + stats = loader.getStatistics(); + assertThat(stats.getLoadCount(), equalTo(2L)); + assertThat(stats.getBatchInvokeCount(), equalTo(1L)); + assertThat(stats.getBatchLoadCount(), equalTo(2L)); + assertThat(stats.getBatchLoadExceptionCount(), equalTo(1L)); + assertThat(stats.getLoadErrorCount(), equalTo(0L)); + + loader.load("error1"); + loader.load("error2"); + loader.load("error3"); + + loader.dispatch(); + + stats = loader.getStatistics(); + assertThat(stats.getLoadCount(), equalTo(5L)); + assertThat(stats.getBatchLoadCount(), equalTo(5L)); + assertThat(stats.getBatchLoadExceptionCount(), equalTo(1L)); + assertThat(stats.getLoadErrorCount(), equalTo(3L)); + + loader.load("bang"); + + loader.dispatch(); + + stats = loader.getStatistics(); + assertThat(stats.getLoadCount(), equalTo(6L)); + assertThat(stats.getBatchLoadCount(), equalTo(6L)); + assertThat(stats.getBatchLoadExceptionCount(), equalTo(2L)); + assertThat(stats.getLoadErrorCount(), equalTo(3L)); + } +} diff --git a/src/test/java/org/dataloader/DataLoaderTest.java b/src/test/java/org/dataloader/DataLoaderTest.java index 4f9f44c..da9ebff 100644 --- a/src/test/java/org/dataloader/DataLoaderTest.java +++ b/src/test/java/org/dataloader/DataLoaderTest.java @@ -998,6 +998,7 @@ public void should_handle_Trys_coming_back_from_batchLoader() throws Exception { assertThat(batchKeyCalls, equalTo(singletonList(asList("A", "B", "bang")))); } + private static CacheKey getJsonObjectCacheMapFn() { return key -> key.stream() .map(entry -> entry.getKey() + ":" + entry.getValue()) diff --git a/src/test/java/org/dataloader/stats/StatisticsCollectorTest.java b/src/test/java/org/dataloader/stats/StatisticsCollectorTest.java new file mode 100644 index 0000000..2b5f5df --- /dev/null +++ b/src/test/java/org/dataloader/stats/StatisticsCollectorTest.java @@ -0,0 +1,213 @@ +package org.dataloader.stats; + +import org.junit.Test; + +import java.util.concurrent.CompletableFuture; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; + +public class StatisticsCollectorTest { + + @Test + public void basic_collection() throws Exception { + StatisticsCollector collector = new SimpleStatisticsCollector(); + + assertThat(collector.getStatistics().getLoadCount(), equalTo(0L)); + assertThat(collector.getStatistics().getBatchLoadCount(), equalTo(0L)); + assertThat(collector.getStatistics().getCacheHitCount(), equalTo(0L)); + assertThat(collector.getStatistics().getCacheMissCount(), equalTo(0L)); + assertThat(collector.getStatistics().getBatchLoadExceptionCount(), equalTo(0L)); + assertThat(collector.getStatistics().getLoadErrorCount(), equalTo(0L)); + + + collector.incrementLoadCount(); + collector.incrementBatchLoadCountBy(1); + collector.incrementCacheHitCount(); + collector.incrementBatchLoadExceptionCount(); + collector.incrementLoadErrorCount(); + + assertThat(collector.getStatistics().getLoadCount(), equalTo(1L)); + assertThat(collector.getStatistics().getBatchLoadCount(), equalTo(1L)); + assertThat(collector.getStatistics().getCacheHitCount(), equalTo(1L)); + assertThat(collector.getStatistics().getCacheMissCount(), equalTo(0L)); + assertThat(collector.getStatistics().getBatchLoadExceptionCount(), equalTo(1L)); + assertThat(collector.getStatistics().getLoadErrorCount(), equalTo(1L)); + } + + @Test + public void ratios_work() throws Exception { + + StatisticsCollector collector = new SimpleStatisticsCollector(); + + collector.incrementLoadCount(); + + Statistics stats = collector.getStatistics(); + assertThat(stats.getBatchLoadRatio(), equalTo(0d)); + assertThat(stats.getCacheHitRatio(), equalTo(0d)); + + + collector.incrementLoadCount(); + collector.incrementLoadCount(); + collector.incrementLoadCount(); + collector.incrementBatchLoadCountBy(1); + + stats = collector.getStatistics(); + assertThat(stats.getBatchLoadRatio(), equalTo(1d / 4d)); + + + collector.incrementLoadCount(); + collector.incrementLoadCount(); + collector.incrementLoadCount(); + collector.incrementCacheHitCount(); + collector.incrementCacheHitCount(); + + stats = collector.getStatistics(); + assertThat(stats.getCacheHitRatio(), equalTo(2d / 7d)); + + collector.incrementLoadCount(); + collector.incrementLoadCount(); + collector.incrementLoadCount(); + collector.incrementBatchLoadExceptionCount(); + collector.incrementBatchLoadExceptionCount(); + + stats = collector.getStatistics(); + assertThat(stats.getBatchLoadExceptionRatio(), equalTo(2d / 10d)); + + collector.incrementLoadCount(); + collector.incrementLoadCount(); + collector.incrementLoadCount(); + collector.incrementLoadErrorCount(); + collector.incrementLoadErrorCount(); + collector.incrementLoadErrorCount(); + + stats = collector.getStatistics(); + assertThat(stats.getLoadErrorRatio(), equalTo(3d / 13d)); + } + + @Test + public void thread_local_collection() throws Exception { + + final ThreadLocalStatisticsCollector collector = new ThreadLocalStatisticsCollector(); + + assertThat(collector.getStatistics().getLoadCount(), equalTo(0L)); + assertThat(collector.getStatistics().getBatchLoadCount(), equalTo(0L)); + assertThat(collector.getStatistics().getCacheHitCount(), equalTo(0L)); + + + collector.incrementLoadCount(); + collector.incrementBatchLoadCountBy(1); + collector.incrementCacheHitCount(); + + assertThat(collector.getStatistics().getLoadCount(), equalTo(1L)); + assertThat(collector.getStatistics().getBatchLoadCount(), equalTo(1L)); + assertThat(collector.getStatistics().getCacheHitCount(), equalTo(1L)); + + assertThat(collector.getOverallStatistics().getLoadCount(), equalTo(1L)); + assertThat(collector.getOverallStatistics().getBatchLoadCount(), equalTo(1L)); + assertThat(collector.getOverallStatistics().getCacheHitCount(), equalTo(1L)); + + CompletableFuture.supplyAsync(() -> { + + collector.incrementLoadCount(); + collector.incrementBatchLoadCountBy(1); + collector.incrementCacheHitCount(); + + // per thread stats here + assertThat(collector.getStatistics().getLoadCount(), equalTo(1L)); + assertThat(collector.getStatistics().getBatchLoadCount(), equalTo(1L)); + assertThat(collector.getStatistics().getCacheHitCount(), equalTo(1L)); + + // overall stats + assertThat(collector.getOverallStatistics().getLoadCount(), equalTo(2L)); + assertThat(collector.getOverallStatistics().getBatchLoadCount(), equalTo(2L)); + assertThat(collector.getOverallStatistics().getCacheHitCount(), equalTo(2L)); + + return null; + }).join(); + + // back on this main thread + + collector.incrementLoadCount(); + collector.incrementBatchLoadCountBy(1); + collector.incrementCacheHitCount(); + + // per thread stats here + assertThat(collector.getStatistics().getLoadCount(), equalTo(2L)); + assertThat(collector.getStatistics().getBatchLoadCount(), equalTo(2L)); + assertThat(collector.getStatistics().getCacheHitCount(), equalTo(2L)); + + // overall stats + assertThat(collector.getOverallStatistics().getLoadCount(), equalTo(3L)); + assertThat(collector.getOverallStatistics().getBatchLoadCount(), equalTo(3L)); + assertThat(collector.getOverallStatistics().getCacheHitCount(), equalTo(3L)); + + + // stats can be reset per thread + collector.resetThread(); + + // thread is reset + assertThat(collector.getStatistics().getLoadCount(), equalTo(0L)); + assertThat(collector.getStatistics().getBatchLoadCount(), equalTo(0L)); + assertThat(collector.getStatistics().getCacheHitCount(), equalTo(0L)); + + // but not overall stats + assertThat(collector.getOverallStatistics().getLoadCount(), equalTo(3L)); + assertThat(collector.getOverallStatistics().getBatchLoadCount(), equalTo(3L)); + assertThat(collector.getOverallStatistics().getCacheHitCount(), equalTo(3L)); + } + + @Test + public void delegating_collector_works() throws Exception { + SimpleStatisticsCollector delegate = new SimpleStatisticsCollector(); + DelegatingStatisticsCollector collector = new DelegatingStatisticsCollector(delegate); + + assertThat(collector.getStatistics().getLoadCount(), equalTo(0L)); + assertThat(collector.getStatistics().getBatchLoadCount(), equalTo(0L)); + assertThat(collector.getStatistics().getCacheHitCount(), equalTo(0L)); + assertThat(collector.getStatistics().getCacheMissCount(), equalTo(0L)); + + + collector.incrementLoadCount(); + collector.incrementBatchLoadCountBy(1); + collector.incrementCacheHitCount(); + collector.incrementBatchLoadExceptionCount(); + collector.incrementLoadErrorCount(); + + assertThat(collector.getStatistics().getLoadCount(), equalTo(1L)); + assertThat(collector.getStatistics().getBatchLoadCount(), equalTo(1L)); + assertThat(collector.getStatistics().getCacheHitCount(), equalTo(1L)); + assertThat(collector.getStatistics().getCacheMissCount(), equalTo(0L)); + assertThat(collector.getStatistics().getBatchLoadExceptionCount(), equalTo(1L)); + assertThat(collector.getStatistics().getLoadErrorCount(), equalTo(1L)); + + assertThat(collector.getDelegateStatistics().getLoadCount(), equalTo(1L)); + assertThat(collector.getDelegateStatistics().getBatchLoadCount(), equalTo(1L)); + assertThat(collector.getDelegateStatistics().getCacheHitCount(), equalTo(1L)); + assertThat(collector.getDelegateStatistics().getCacheMissCount(), equalTo(0L)); + assertThat(collector.getDelegateStatistics().getBatchLoadExceptionCount(), equalTo(1L)); + assertThat(collector.getDelegateStatistics().getLoadErrorCount(), equalTo(1L)); + + assertThat(delegate.getStatistics().getLoadCount(), equalTo(1L)); + assertThat(delegate.getStatistics().getBatchLoadCount(), equalTo(1L)); + assertThat(delegate.getStatistics().getCacheHitCount(), equalTo(1L)); + assertThat(delegate.getStatistics().getCacheMissCount(), equalTo(0L)); + assertThat(delegate.getStatistics().getBatchLoadExceptionCount(), equalTo(1L)); + assertThat(delegate.getStatistics().getLoadErrorCount(), equalTo(1L)); + } + + @Test + public void noop_is_just_that() throws Exception { + StatisticsCollector collector = new NoOpStatisticsCollector(); + collector.incrementLoadErrorCount(); + collector.incrementBatchLoadExceptionCount(); + collector.incrementBatchLoadCountBy(1); + collector.incrementCacheHitCount(); + + assertThat(collector.getStatistics().getLoadCount(), equalTo(0L)); + assertThat(collector.getStatistics().getBatchLoadCount(), equalTo(0L)); + assertThat(collector.getStatistics().getCacheHitCount(), equalTo(0L)); + assertThat(collector.getStatistics().getCacheMissCount(), equalTo(0L)); + + } +} \ No newline at end of file diff --git a/src/test/java/org/dataloader/stats/StatisticsTest.java b/src/test/java/org/dataloader/stats/StatisticsTest.java new file mode 100644 index 0000000..b900807 --- /dev/null +++ b/src/test/java/org/dataloader/stats/StatisticsTest.java @@ -0,0 +1,45 @@ +package org.dataloader.stats; + +import org.junit.Test; + +import java.util.Map; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; + +public class StatisticsTest { + + @Test + public void combine_works() throws Exception { + Statistics one = new Statistics(1, 5, 6, 2, 4, 3); + Statistics two = new Statistics(6, 10, 6, 7, 9, 8); + + Statistics combine = one.combine(two); + + assertThat(combine.getLoadCount(), equalTo(7L)); + assertThat(combine.getBatchLoadCount(), equalTo(9L)); + assertThat(combine.getCacheHitCount(), equalTo(11L)); + assertThat(combine.getBatchLoadExceptionCount(), equalTo(13L)); + assertThat(combine.getLoadErrorCount(), equalTo(15L)); + assertThat(combine.getBatchInvokeCount(), equalTo(12L)); + } + + @Test + public void to_map_works() throws Exception { + + Statistics one = new Statistics(10, 2, 11, 3, 4, 5); + Map map = one.toMap(); + + assertThat(map.get("loadCount"), equalTo(10L)); + assertThat(map.get("loadErrorCount"), equalTo(2L)); + assertThat(map.get("loadErrorRatio"), equalTo(0.2d)); + assertThat(map.get("batchInvokeCount"), equalTo(11L)); + assertThat(map.get("batchLoadCount"), equalTo(3L)); + assertThat(map.get("batchLoadRatio"), equalTo(0.3d)); + assertThat(map.get("batchLoadExceptionCount"), equalTo(4L)); + assertThat(map.get("batchLoadExceptionRatio"), equalTo(0.4d)); + assertThat(map.get("cacheHitCount"), equalTo(5L)); + assertThat(map.get("cacheHitRatio"), equalTo(0.5d)); + + } +} \ No newline at end of file