From 90086c1d525c56d1f0957836a2e2b3177db3e01d Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Fri, 25 Jun 2021 22:22:38 +1000 Subject: [PATCH] Added a instant since last dispatch value --- src/main/java/org/dataloader/DataLoader.java | 52 +++++++++++++- .../java/org/dataloader/DataLoaderHelper.java | 22 +++++- .../annotations/VisibleForTesting.java | 18 +++++ .../org/dataloader/DataLoaderTimeTest.java | 67 +++++++++++++++++++ 4 files changed, 155 insertions(+), 4 deletions(-) create mode 100644 src/main/java/org/dataloader/annotations/VisibleForTesting.java create mode 100644 src/test/java/org/dataloader/DataLoaderTimeTest.java diff --git a/src/main/java/org/dataloader/DataLoader.java b/src/main/java/org/dataloader/DataLoader.java index 2c194fc..f59d0a1 100644 --- a/src/main/java/org/dataloader/DataLoader.java +++ b/src/main/java/org/dataloader/DataLoader.java @@ -17,10 +17,13 @@ package org.dataloader; import org.dataloader.annotations.PublicApi; +import org.dataloader.annotations.VisibleForTesting; import org.dataloader.impl.CompletableFutureKit; import org.dataloader.stats.Statistics; import org.dataloader.stats.StatisticsCollector; +import java.time.Clock; +import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -52,6 +55,7 @@ * * @param type parameter indicating the type of the data load keys * @param type parameter indicating the type of the data that is returned + * * @author Arnold Schrijver * @author Brad Baker */ @@ -69,6 +73,7 @@ public class DataLoader { * @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) { @@ -82,6 +87,7 @@ public static DataLoader newDataLoader(BatchLoader batchLoadF * @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) { @@ -102,6 +108,7 @@ public static DataLoader newDataLoader(BatchLoader batchLoadF * @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) { @@ -117,7 +124,9 @@ public static DataLoader newDataLoaderWithTry(BatchLoader * @param options the options to use * @param the key type * @param the value type + * * @return a new DataLoader + * * @see #newDataLoaderWithTry(BatchLoader) */ @SuppressWarnings("unchecked") @@ -132,6 +141,7 @@ public static DataLoader newDataLoaderWithTry(BatchLoader * @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) { @@ -145,6 +155,7 @@ public static DataLoader newDataLoader(BatchLoaderWithContext * @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) { @@ -165,6 +176,7 @@ public static DataLoader newDataLoader(BatchLoaderWithContext * @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) { @@ -180,7 +192,9 @@ public static DataLoader newDataLoaderWithTry(BatchLoaderWithContex * @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) { @@ -194,6 +208,7 @@ public static DataLoader newDataLoaderWithTry(BatchLoaderWithContex * @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) { @@ -207,6 +222,7 @@ public static DataLoader newMappedDataLoader(MappedBatchLoader the key type * @param the value type + * * @return a new DataLoader */ public static DataLoader newMappedDataLoader(MappedBatchLoader batchLoadFunction, DataLoaderOptions options) { @@ -228,6 +244,7 @@ public static DataLoader newMappedDataLoader(MappedBatchLoader the key type * @param the value type + * * @return a new DataLoader */ public static DataLoader newMappedDataLoaderWithTry(MappedBatchLoader> batchLoadFunction) { @@ -243,7 +260,9 @@ public static DataLoader newMappedDataLoaderWithTry(MappedBatchLoad * @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) { @@ -257,6 +276,7 @@ public static DataLoader newMappedDataLoaderWithTry(MappedBatchLoad * @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) { @@ -270,6 +290,7 @@ public static DataLoader newMappedDataLoader(MappedBatchLoaderWithC * @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) { @@ -290,6 +311,7 @@ public static DataLoader newMappedDataLoader(MappedBatchLoaderWithC * @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) { @@ -305,7 +327,9 @@ public static DataLoader newMappedDataLoaderWithTry(MappedBatchLoad * @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) { @@ -337,7 +361,12 @@ private DataLoader(Object batchLoadFunction, DataLoaderOptions options) { // order of keys matter in data loader this.stats = nonNull(loaderOptions.getStatisticsCollector()); - this.helper = new DataLoaderHelper<>(this, batchLoadFunction, loaderOptions, this.futureCache, this.stats); + this.helper = new DataLoaderHelper<>(this, batchLoadFunction, loaderOptions, this.futureCache, this.stats, clock()); + } + + @VisibleForTesting + Clock clock() { + return Clock.systemUTC(); } @SuppressWarnings("unchecked") @@ -345,6 +374,17 @@ private CacheMap> determineCacheMap(DataLoaderOptio return loaderOptions.cacheMap().isPresent() ? (CacheMap>) loaderOptions.cacheMap().get() : CacheMap.simpleMap(); } + + /** + * This returns the last instant the data loader was dispatched. When the data loader is created this value is set to now. + * + * @return the instant since the last dispatch + */ + public Instant getLastDispatchTime() { + return helper.getLastDispatchTime(); + } + + /** * Requests to load the data with the specified key asynchronously, and returns a future of the resulting value. *

@@ -353,6 +393,7 @@ private CacheMap> determineCacheMap(DataLoaderOptio * and returned from cache). * * @param key the key to load + * * @return the future of the value */ public CompletableFuture load(K key) { @@ -370,6 +411,7 @@ public CompletableFuture load(K key) { * 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) { @@ -388,6 +430,7 @@ public Optional> getIfPresent(K key) { * 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) { @@ -407,6 +450,7 @@ public Optional> getIfCompleted(K key) { * * @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) { @@ -422,6 +466,7 @@ public CompletableFuture load(K key, Object keyContext) { * 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) { @@ -441,6 +486,7 @@ public CompletableFuture> loadMany(List keys) { * * @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) { @@ -517,6 +563,7 @@ public int dispatchDepth() { * on the next load request. * * @param key the key to remove + * * @return the data loader for fluent coding */ public DataLoader clear(K key) { @@ -544,6 +591,7 @@ public DataLoader clearAll() { * * @param key the key * @param value the value + * * @return the data loader for fluent coding */ public DataLoader prime(K key, V value) { @@ -561,6 +609,7 @@ public DataLoader prime(K key, V value) { * * @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) { @@ -578,6 +627,7 @@ public DataLoader prime(K key, Exception error) { * 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) { diff --git a/src/main/java/org/dataloader/DataLoaderHelper.java b/src/main/java/org/dataloader/DataLoaderHelper.java index cecfc18..ea1ec57 100644 --- a/src/main/java/org/dataloader/DataLoaderHelper.java +++ b/src/main/java/org/dataloader/DataLoaderHelper.java @@ -4,6 +4,8 @@ import org.dataloader.impl.CompletableFutureKit; import org.dataloader.stats.StatisticsCollector; +import java.time.Clock; +import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashSet; @@ -13,6 +15,7 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import static java.util.Collections.emptyList; @@ -30,7 +33,6 @@ @Internal class DataLoaderHelper { - static class LoaderQueueEntry { final K key; @@ -62,14 +64,27 @@ Object getCallContext() { private final CacheMap> futureCache; private final List>> loaderQueue; private final StatisticsCollector stats; + private final Clock clock; + private final AtomicReference lastDispatchTime; - DataLoaderHelper(DataLoader dataLoader, Object batchLoadFunction, DataLoaderOptions loaderOptions, CacheMap> futureCache, StatisticsCollector stats) { + DataLoaderHelper(DataLoader dataLoader, Object batchLoadFunction, DataLoaderOptions loaderOptions, CacheMap> futureCache, StatisticsCollector stats, Clock clock) { this.dataLoader = dataLoader; this.batchLoadFunction = batchLoadFunction; this.loaderOptions = loaderOptions; this.futureCache = futureCache; this.loaderQueue = new ArrayList<>(); this.stats = stats; + this.clock = clock; + this.lastDispatchTime = new AtomicReference<>(); + this.lastDispatchTime.set(now()); + } + + private Instant now() { + return Instant.now(clock); + } + + public Instant getLastDispatchTime() { + return lastDispatchTime.get(); } Optional> getIfPresent(K key) { @@ -146,7 +161,7 @@ Object getCacheKey(K key) { @SuppressWarnings("unchecked") Object getCacheKeyWithContext(K key, Object context) { return loaderOptions.cacheKeyFunction().isPresent() ? - loaderOptions.cacheKeyFunction().get().getKeyWithContext(key, context): key; + loaderOptions.cacheKeyFunction().get().getKeyWithContext(key, context) : key; } DispatchResult dispatch() { @@ -163,6 +178,7 @@ DispatchResult dispatch() { callContexts.add(entry.getCallContext()); }); loaderQueue.clear(); + lastDispatchTime.set(now()); } if (!batchingEnabled || keys.isEmpty()) { return new DispatchResult<>(CompletableFuture.completedFuture(emptyList()), 0); diff --git a/src/main/java/org/dataloader/annotations/VisibleForTesting.java b/src/main/java/org/dataloader/annotations/VisibleForTesting.java new file mode 100644 index 0000000..99f97f0 --- /dev/null +++ b/src/main/java/org/dataloader/annotations/VisibleForTesting.java @@ -0,0 +1,18 @@ +package org.dataloader.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; + +/** + * Marks fields, methods etc as more visible than actually needed for testing purposes. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(value = {CONSTRUCTOR, METHOD, FIELD}) +@Internal +public @interface VisibleForTesting { +} diff --git a/src/test/java/org/dataloader/DataLoaderTimeTest.java b/src/test/java/org/dataloader/DataLoaderTimeTest.java new file mode 100644 index 0000000..c6f1af2 --- /dev/null +++ b/src/test/java/org/dataloader/DataLoaderTimeTest.java @@ -0,0 +1,67 @@ +package org.dataloader; + +import org.junit.Test; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; + +@SuppressWarnings("UnusedReturnValue") +public class DataLoaderTimeTest { + + private BatchLoader keysAsValues() { + return CompletableFuture::completedFuture; + } + + AtomicReference clockRef = new AtomicReference<>(); + + @Test + public void should_set_and_instant_if_dispatched() { + Clock clock = zeroEpoch(); + clockRef.set(clock); + + Instant startInstant = now(); + + DataLoader dataLoader = new DataLoader(keysAsValues()) { + @Override + Clock clock() { + return clockRef.get(); + } + }; + + long sinceMS = msSince(dataLoader.getLastDispatchTime()); + assertThat(sinceMS, equalTo(0L)); + assertThat(startInstant, equalTo(dataLoader.getLastDispatchTime())); + + jump(clock, 1000); + dataLoader.dispatch(); + + sinceMS = msSince(dataLoader.getLastDispatchTime()); + assertThat(sinceMS, equalTo(1000L)); + } + + private long msSince(Instant lastDispatchTime) { + return Duration.between(lastDispatchTime, now()).toMillis(); + } + + private Instant now() { + return Instant.now(clockRef.get()); + } + + private Clock jump(Clock clock, int millis) { + clock = Clock.offset(clock, Duration.ofMillis(millis)); + clockRef.set(clock); + return clock; + } + + private Clock zeroEpoch() { + return Clock.fixed(Instant.ofEpochMilli(0), ZoneId.systemDefault()); + } + +}