Skip to content

Commit 6a42943

Browse files
committed
DataLoader main class implementation
1 parent 195825b commit 6a42943

File tree

1 file changed

+204
-0
lines changed

1 file changed

+204
-0
lines changed
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
/*
2+
* Copyright (c) 2016 The original author or authors
3+
*
4+
* All rights reserved. This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Public License v1.0
6+
* and Apache License v2.0 which accompanies this distribution.
7+
*
8+
* The Eclipse Public License is available at
9+
* http://www.eclipse.org/legal/epl-v10.html
10+
*
11+
* The Apache License v2.0 is available at
12+
* http://www.opensource.org/licenses/apache2.0.php
13+
*
14+
* You may elect to redistribute this code under either of these licenses.
15+
*/
16+
17+
package io.engagingspaces.vertx.dataloader;
18+
19+
import io.vertx.core.CompositeFuture;
20+
import io.vertx.core.Future;
21+
22+
import java.util.Collections;
23+
import java.util.LinkedHashMap;
24+
import java.util.List;
25+
import java.util.Objects;
26+
import java.util.concurrent.atomic.AtomicInteger;
27+
import java.util.stream.Collectors;
28+
29+
/**
30+
* Data loader is a utility class that allows batch loading of data that is identified by a set of unique keys. For
31+
* each key that is loaded a separate {@link Future} is returned, that completes as the batch function completes.
32+
* Besides individual futures a {@link CompositeFuture} of the batch is available as well.
33+
* <p>
34+
* With batching enabled the execution will start after calling {@link DataLoader#dispatch()}, causing the queue of
35+
* loaded keys to be sent to the batch function, clears the queue, and returns the {@link CompositeFuture}.
36+
* <p>
37+
* As batch functions are executed the resulting futures are cached using a cache implementation of choice, so they
38+
* will only execute once. Individual cache keys can be cleared, so they will be re-fetched when referred to again.
39+
* It is also possible to clear the cache entirely, and prime it with values before they are used.
40+
* <p>
41+
* Both caching and batching can be disabled. Configuration of the data loader is done by providing a
42+
* {@link DataLoaderOptions} instance on creation.
43+
*
44+
* @param <K> type parameter indicating the type of the data load keys
45+
* @param <V> type parameter indicating the type of the data that is returned
46+
*
47+
* @author <a href="https://github.com/aschrijver/">Arnold Schrijver</a>
48+
*/
49+
public class DataLoader<K, V> {
50+
51+
private final BatchLoader<K> batchLoadFunction;
52+
private final DataLoaderOptions loaderOptions;
53+
private final CacheMap<Object, Future<V>> futureCache;
54+
private final LinkedHashMap<K, Future<V>> loaderQueue;
55+
56+
/**
57+
* Creates a new data loader with the provided batch load function, and default options.
58+
*
59+
* @param batchLoadFunction the batch load function to use
60+
*/
61+
public DataLoader(BatchLoader<K> batchLoadFunction) {
62+
this(batchLoadFunction, null);
63+
}
64+
65+
/**
66+
* Creates a new data loader with the provided batch load function and options.
67+
*
68+
* @param batchLoadFunction the batch load function to use
69+
* @param options the batch load options
70+
*/
71+
public DataLoader(BatchLoader<K> batchLoadFunction, DataLoaderOptions options) {
72+
Objects.requireNonNull(batchLoadFunction, "Batch load function cannot be null");
73+
this.batchLoadFunction = batchLoadFunction;
74+
this.loaderOptions = options == null ? new DataLoaderOptions() : options;
75+
this.futureCache = loaderOptions.cacheMap().isPresent() ? loaderOptions.cacheMap().get() : CacheMap.simpleMap();
76+
this.loaderQueue = new LinkedHashMap<>();
77+
}
78+
79+
/**
80+
* Requests to load the data with the specified key asynchronously, and returns a future of the resulting value.
81+
* <p>
82+
* If batching is enabled (the default), you'll have to call {@link DataLoader#dispatch()} at a later stage to
83+
* start batch execution. If you forget this call the future will never be completed (unless already completed,
84+
* and returned from cache).
85+
*
86+
* @param key the key to load
87+
* @return the future of the value
88+
*/
89+
public Future<V> load(K key) {
90+
Objects.requireNonNull(key, "Key cannot be null");
91+
Object cacheKey = getCacheKey(key);
92+
if (loaderOptions.cachingEnabled() && futureCache.containsKey(cacheKey)) {
93+
return futureCache.get(key);
94+
}
95+
96+
Future<V> future = Future.future();
97+
if (loaderOptions.batchingEnabled()) {
98+
loaderQueue.put(key, future);
99+
} else {
100+
CompositeFuture compositeFuture = batchLoadFunction.load(Collections.singleton(key));
101+
if (compositeFuture.succeeded()) {
102+
future.complete(compositeFuture.result().result(0));
103+
} else {
104+
future.fail(compositeFuture.cause());
105+
}
106+
}
107+
if (loaderOptions.cachingEnabled()) {
108+
futureCache.set(key, future);
109+
}
110+
return future;
111+
}
112+
113+
/**
114+
* Requests to load the list of data provided by the specified keys asynchronously, and returns a composite future
115+
* of the resulting values.
116+
* <p>
117+
* If batching is enabled (the default), you'll have to call {@link DataLoader#dispatch()} at a later stage to
118+
* start batch execution. If you forget this call the future will never be completed (unless already completed,
119+
* and returned from cache).
120+
*
121+
* @param keys the list of keys to load
122+
* @return the composite future of the list of values
123+
*/
124+
public CompositeFuture loadMany(List<K> keys) {
125+
return CompositeFuture.all(keys.stream().map(this::load).collect(Collectors.toList()));
126+
}
127+
128+
/**
129+
* Dispatches the queued load requests to the batch execution function and returns a composite future of the result.
130+
* <p>
131+
* If batching is disabled, or there are no queued requests, then a succeeded composite future is returned.
132+
*
133+
* @return the composite future of the queued load requests
134+
*/
135+
public CompositeFuture dispatch() {
136+
if (!loaderOptions.batchingEnabled() || loaderQueue.size() == 0) {
137+
return CompositeFuture.all(Collections.emptyList());
138+
}
139+
CompositeFuture batch = batchLoadFunction.load(loaderQueue.keySet());
140+
AtomicInteger index = new AtomicInteger(0);
141+
loaderQueue.forEach((key, future) -> {
142+
if (batch.succeeded(index.get())) {
143+
future.complete(batch.result(index.get()));
144+
} else {
145+
future.fail(batch.cause(index.get()));
146+
}
147+
index.incrementAndGet();
148+
});
149+
loaderQueue.clear();
150+
return batch;
151+
}
152+
153+
/**
154+
* Clears the future with the specified key from the cache, if caching is enabled, so it will be re-fetched
155+
* on the next load request.
156+
*
157+
* @param key the key to remove
158+
* @return the data loader for fluent coding
159+
*/
160+
public DataLoader<K, V> clear(K key) {
161+
Object cacheKey = getCacheKey(key);
162+
futureCache.delete(cacheKey);
163+
return this;
164+
}
165+
166+
/**
167+
* Clears the entire cache map of the loader.
168+
*
169+
* @return the data loader for fluent coding
170+
*/
171+
public DataLoader<K, V> clearAll() {
172+
futureCache.clear();
173+
return this;
174+
}
175+
176+
/**
177+
* Primes the cache with the given key and value.
178+
*
179+
* @param key the key
180+
* @param value the value
181+
* @return the data loader for fluent coding
182+
*/
183+
public DataLoader<K, V> prime(K key, V value) {
184+
Object cacheKey = getCacheKey(key);
185+
if (!futureCache.containsKey(cacheKey)) {
186+
futureCache.set(cacheKey, Future.succeededFuture(value));
187+
}
188+
return this;
189+
}
190+
191+
/**
192+
* Gets the object that is used in the internal cache map as key, by applying the cache key function to
193+
* the provided key.
194+
* <p>
195+
* If no cache key function is present in {@link DataLoaderOptions}, then the returned value equals the input key.
196+
*
197+
* @param key the input key
198+
* @return the cache key after the input is transformed with the cache key function
199+
*/
200+
public Object getCacheKey(K key) {
201+
return loaderOptions.cacheKeyFunction().isPresent() ?
202+
loaderOptions.cacheKeyFunction().get().getKey(key) : key;
203+
}
204+
}

0 commit comments

Comments
 (0)