Skip to content

Commit aa2994e

Browse files
authored
Added statistics support (#11)
* Added statistics support * added delegating stats collector * Added error counts to stats * Added noop version of collector * Made it doubles and added toMap * javadoc warning * batch load count is now the number of objects loaded via the batch function and batch invoke count the number of invocations of the batch function * readme updates * Made Statistics a class
1 parent 25047ce commit aa2994e

17 files changed

+1096
-13
lines changed

README.md

+32
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,38 @@ In some circumstances you may wish to clear the cache for these individual probl
269269
});
270270
```
271271

272+
273+
## Statistics on what is happening
274+
275+
`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
276+
asked for via batching and so on.
277+
278+
Knowing what the behaviour of your data is important for you to understand how efficient you are in serving the data via this pattern.
279+
280+
281+
```java
282+
Statistics statistics = userDataLoader.getStatistics();
283+
284+
System.out.println(format("load : %d", statistics.getLoadCount()));
285+
System.out.println(format("batch load: %d", statistics.getBatchLoadCount()));
286+
System.out.println(format("cache hit: %d", statistics.getCacheHitCount()));
287+
System.out.println(format("cache hit ratio: %d", statistics.getCacheHitRatio()));
288+
289+
```
290+
291+
`DataLoaderRegistry` can also roll up the statistics for all data loaders inside it.
292+
293+
You can configure the statistics collector used when you build the data loader
294+
295+
```java
296+
DataLoaderOptions options = DataLoaderOptions.newOptions().setStatisticsCollector(() -> new ThreadLocalStatisticsCollector());
297+
DataLoader<String,User> userDataLoader = DataLoader.newDataLoader(userBatchLoader,options);
298+
299+
```
300+
301+
Which collector you use is up to you. It ships with the following: `SimpleStatisticsCollector`, `ThreadLocalStatisticsCollector`, `DelegatingStatisticsCollector`
302+
and `NoOpStatisticsCollector`.
303+
272304
## The scope of a data loader is important
273305

274306
If you are serving web requests then the data can be specific to the user requesting it. If you have user specific data

src/main/java/org/dataloader/DataLoader.java

+37-10
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,16 @@
1717
package org.dataloader;
1818

1919
import org.dataloader.impl.CompletableFutureKit;
20+
import org.dataloader.stats.Statistics;
21+
import org.dataloader.stats.StatisticsCollector;
2022

2123
import java.util.ArrayList;
2224
import java.util.Collection;
2325
import java.util.LinkedHashMap;
2426
import java.util.List;
2527
import java.util.Map;
2628
import java.util.concurrent.CompletableFuture;
29+
import java.util.concurrent.CompletionStage;
2730
import java.util.stream.Collectors;
2831

2932
import static java.util.Collections.emptyList;
@@ -64,6 +67,7 @@ public class DataLoader<K, V> {
6467
private final DataLoaderOptions loaderOptions;
6568
private final CacheMap<Object, CompletableFuture<V>> futureCache;
6669
private final Map<K, CompletableFuture<V>> loaderQueue;
70+
private final StatisticsCollector stats;
6771

6872
/**
6973
* Creates new DataLoader with the specified batch loader function and default options
@@ -153,6 +157,7 @@ public DataLoader(BatchLoader<K, V> batchLoadFunction, DataLoaderOptions options
153157
this.futureCache = determineCacheMap(loaderOptions);
154158
// order of keys matter in data loader
155159
this.loaderQueue = new LinkedHashMap<>();
160+
this.stats = nonNull(this.loaderOptions.getStatisticsCollector());
156161
}
157162

158163
@SuppressWarnings("unchecked")
@@ -173,8 +178,11 @@ private CacheMap<Object, CompletableFuture<V>> determineCacheMap(DataLoaderOptio
173178
*/
174179
public CompletableFuture<V> load(K key) {
175180
Object cacheKey = getCacheKey(nonNull(key));
181+
stats.incrementLoadCount();
182+
176183
synchronized (futureCache) {
177184
if (loaderOptions.cachingEnabled() && futureCache.containsKey(cacheKey)) {
185+
stats.incrementCacheHitCount();
178186
return futureCache.get(cacheKey);
179187
}
180188
}
@@ -185,6 +193,7 @@ public CompletableFuture<V> load(K key) {
185193
loaderQueue.put(key, future);
186194
}
187195
} else {
196+
stats.incrementBatchLoadCountBy(1);
188197
// immediate execution of batch function
189198
CompletableFuture<List<V>> batchedLoad = batchLoadFunction
190199
.load(singletonList(key))
@@ -291,7 +300,14 @@ private CompletableFuture<List<V>> sliceIntoBatchesOfBatches(List<K> keys, List<
291300

292301
@SuppressWarnings("unchecked")
293302
private CompletableFuture<List<V>> dispatchQueueBatch(List<K> keys, List<CompletableFuture<V>> queuedFutures) {
294-
return batchLoadFunction.load(keys)
303+
stats.incrementBatchLoadCountBy(keys.size());
304+
CompletionStage<List<V>> batchLoad;
305+
try {
306+
batchLoad = nonNull(batchLoadFunction.load(keys), "Your batch loader function MUST return a non null CompletionStage promise");
307+
} catch (Exception e) {
308+
batchLoad = CompletableFutureKit.failedFuture(e);
309+
}
310+
return batchLoad
295311
.toCompletableFuture()
296312
.thenApply(values -> {
297313
assertState(keys.size() == values.size(), "The size of the promised values MUST be the same size as the key list");
@@ -300,20 +316,28 @@ private CompletableFuture<List<V>> dispatchQueueBatch(List<K> keys, List<Complet
300316
Object value = values.get(idx);
301317
CompletableFuture<V> future = queuedFutures.get(idx);
302318
if (value instanceof Throwable) {
319+
stats.incrementLoadErrorCount();
303320
future.completeExceptionally((Throwable) value);
304321
// we don't clear the cached view of this entry to avoid
305322
// frequently loading the same error
306323
} else if (value instanceof Try) {
307324
// we allow the batch loader to return a Try so we can better represent a computation
308325
// that might have worked or not.
309-
handleTry((Try<V>) value, future);
326+
Try<V> tryValue = (Try<V>) value;
327+
if (tryValue.isSuccess()) {
328+
future.complete(tryValue.get());
329+
} else {
330+
stats.incrementLoadErrorCount();
331+
future.completeExceptionally(tryValue.getThrowable());
332+
}
310333
} else {
311334
V val = (V) value;
312335
future.complete(val);
313336
}
314337
}
315338
return values;
316339
}).exceptionally(ex -> {
340+
stats.incrementBatchLoadExceptionCount();
317341
for (int idx = 0; idx < queuedFutures.size(); idx++) {
318342
K key = keys.get(idx);
319343
CompletableFuture<V> future = queuedFutures.get(idx);
@@ -325,14 +349,6 @@ private CompletableFuture<List<V>> dispatchQueueBatch(List<K> keys, List<Complet
325349
});
326350
}
327351

328-
private void handleTry(Try<V> vTry, CompletableFuture<V> future) {
329-
if (vTry.isSuccess()) {
330-
future.complete(vTry.get());
331-
} else {
332-
future.completeExceptionally(vTry.getThrowable());
333-
}
334-
}
335-
336352
/**
337353
* Normally {@link #dispatch()} is an asynchronous operation but this version will 'join' on the
338354
* 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) {
441457
return loaderOptions.cacheKeyFunction().isPresent() ?
442458
loaderOptions.cacheKeyFunction().get().getKey(key) : key;
443459
}
460+
461+
/**
462+
* Gets the statistics associated with this data loader. These will have been gather via
463+
* the {@link org.dataloader.stats.StatisticsCollector} passed in via {@link DataLoaderOptions#getStatisticsCollector()}
464+
*
465+
* @return statistics for this data loader
466+
*/
467+
public Statistics getStatistics() {
468+
return stats.getStatistics();
469+
}
470+
444471
}

src/main/java/org/dataloader/DataLoaderOptions.java

+32-2
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@
1616

1717
package org.dataloader;
1818

19-
import org.dataloader.impl.Assertions;
19+
import org.dataloader.stats.SimpleStatisticsCollector;
20+
import org.dataloader.stats.StatisticsCollector;
2021

2122
import java.util.Optional;
23+
import java.util.function.Supplier;
24+
25+
import static org.dataloader.impl.Assertions.nonNull;
2226

2327
/**
2428
* Configuration options for {@link DataLoader} instances.
@@ -32,6 +36,7 @@ public class DataLoaderOptions {
3236
private CacheKey cacheKeyFunction;
3337
private CacheMap cacheMap;
3438
private int maxBatchSize;
39+
private Supplier<StatisticsCollector> statisticsCollector;
3540

3641
/**
3742
* Creates a new data loader options with default settings.
@@ -40,6 +45,7 @@ public DataLoaderOptions() {
4045
batchingEnabled = true;
4146
cachingEnabled = true;
4247
maxBatchSize = -1;
48+
statisticsCollector = SimpleStatisticsCollector::new;
4349
}
4450

4551
/**
@@ -48,12 +54,13 @@ public DataLoaderOptions() {
4854
* @param other the other options instance
4955
*/
5056
public DataLoaderOptions(DataLoaderOptions other) {
51-
Assertions.nonNull(other);
57+
nonNull(other);
5258
this.batchingEnabled = other.batchingEnabled;
5359
this.cachingEnabled = other.cachingEnabled;
5460
this.cacheKeyFunction = other.cacheKeyFunction;
5561
this.cacheMap = other.cacheMap;
5662
this.maxBatchSize = other.maxBatchSize;
63+
this.statisticsCollector = other.statisticsCollector;
5764
}
5865

5966
/**
@@ -173,4 +180,27 @@ public DataLoaderOptions setMaxBatchSize(int maxBatchSize) {
173180
this.maxBatchSize = maxBatchSize;
174181
return this;
175182
}
183+
184+
/**
185+
* @return the statistics collector to use with these options
186+
*/
187+
public StatisticsCollector getStatisticsCollector() {
188+
return nonNull(this.statisticsCollector.get());
189+
}
190+
191+
/**
192+
* Sets the statistics collector supplier that will be used with these data loader options. Since it uses
193+
* the supplier pattern, you can create a new statistics collector on each call or you can reuse
194+
* a common value
195+
*
196+
* @param statisticsCollector the statistics collector to use
197+
*
198+
* @return the data loader options for fluent coding
199+
*/
200+
public DataLoaderOptions setStatisticsCollector(Supplier<StatisticsCollector> statisticsCollector) {
201+
this.statisticsCollector = nonNull(statisticsCollector);
202+
return this;
203+
}
204+
205+
176206
}

src/main/java/org/dataloader/DataLoaderRegistry.java

+14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package org.dataloader;
22

3+
import org.dataloader.stats.Statistics;
4+
35
import java.util.ArrayList;
46
import java.util.HashSet;
57
import java.util.List;
@@ -91,4 +93,16 @@ public Set<String> getKeys() {
9193
public void dispatchAll() {
9294
getDataLoaders().forEach(DataLoader::dispatch);
9395
}
96+
97+
/**
98+
* @return a combined set of statistics for all data loaders in this registry presented
99+
* as the sum of all their statistics
100+
*/
101+
public Statistics getStatistics() {
102+
Statistics stats = new Statistics();
103+
for (DataLoader<?, ?> dataLoader : dataLoaders.values()) {
104+
stats = stats.combine(dataLoader.getStatistics());
105+
}
106+
return stats;
107+
}
94108
}

src/main/java/org/dataloader/impl/Assertions.java

+4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ public static <T> T nonNull(T t) {
1414
return Objects.requireNonNull(t, "nonNull object required");
1515
}
1616

17+
public static <T> T nonNull(T t, String message) {
18+
return Objects.requireNonNull(t, message);
19+
}
20+
1721
private static class AssertionException extends IllegalStateException {
1822
public AssertionException(String message) {
1923
super(message);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package org.dataloader.stats;
2+
3+
import static org.dataloader.impl.Assertions.nonNull;
4+
5+
/**
6+
* This statistics collector keeps dataloader statistics AND also calls the delegate
7+
* collector at the same time. This allows you to keep a specific set of statistics
8+
* and also delegate the calls onto another collector.
9+
*/
10+
public class DelegatingStatisticsCollector implements StatisticsCollector {
11+
12+
private final StatisticsCollector collector = new SimpleStatisticsCollector();
13+
private final StatisticsCollector delegateCollector;
14+
15+
/**
16+
* @param delegateCollector a non null delegate collector
17+
*/
18+
public DelegatingStatisticsCollector(StatisticsCollector delegateCollector) {
19+
this.delegateCollector = nonNull(delegateCollector);
20+
}
21+
22+
@Override
23+
public long incrementLoadCount() {
24+
delegateCollector.incrementLoadCount();
25+
return collector.incrementLoadCount();
26+
}
27+
28+
@Override
29+
public long incrementBatchLoadCountBy(long delta) {
30+
delegateCollector.incrementBatchLoadCountBy(delta);
31+
return collector.incrementBatchLoadCountBy(delta);
32+
}
33+
34+
@Override
35+
public long incrementCacheHitCount() {
36+
delegateCollector.incrementCacheHitCount();
37+
return collector.incrementCacheHitCount();
38+
}
39+
40+
@Override
41+
public long incrementLoadErrorCount() {
42+
delegateCollector.incrementLoadErrorCount();
43+
return collector.incrementLoadErrorCount();
44+
}
45+
46+
@Override
47+
public long incrementBatchLoadExceptionCount() {
48+
delegateCollector.incrementBatchLoadExceptionCount();
49+
return collector.incrementBatchLoadExceptionCount();
50+
}
51+
52+
/**
53+
* @return the statistics of the this collector (and not its delegate)
54+
*/
55+
@Override
56+
public Statistics getStatistics() {
57+
return collector.getStatistics();
58+
}
59+
60+
/**
61+
* @return the statistics of the delegate
62+
*/
63+
public Statistics getDelegateStatistics() {
64+
return delegateCollector.getStatistics();
65+
}
66+
67+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package org.dataloader.stats;
2+
3+
/**
4+
* A statistics collector that does nothing
5+
*/
6+
public class NoOpStatisticsCollector implements StatisticsCollector {
7+
8+
private static final Statistics ZERO_STATS = new Statistics();
9+
10+
@Override
11+
public long incrementLoadCount() {
12+
return 0;
13+
}
14+
15+
@Override
16+
public long incrementLoadErrorCount() {
17+
return 0;
18+
}
19+
20+
@Override
21+
public long incrementBatchLoadCountBy(long delta) {
22+
return 0;
23+
}
24+
25+
@Override
26+
public long incrementBatchLoadExceptionCount() {
27+
return 0;
28+
}
29+
30+
@Override
31+
public long incrementCacheHitCount() {
32+
return 0;
33+
}
34+
35+
@Override
36+
public Statistics getStatistics() {
37+
return ZERO_STATS;
38+
}
39+
}

0 commit comments

Comments
 (0)