From 871d133a375b6773824d0d6ea9d51aafe64f14bd Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 9 Feb 2024 17:00:00 -0600 Subject: [PATCH] CE-847 Add overload of getResult that takes the lookup function to use if not found - much more clear & useful. --- .../core/utils/memoization/Memoization.java | 104 +++++++++++++++++- .../utils/memoization/MemoizationTest.java | 55 +++++++++ 2 files changed, 158 insertions(+), 1 deletion(-) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/memoization/Memoization.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/memoization/Memoization.java index e874c5e5..36ae4aa0 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/memoization/Memoization.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/memoization/Memoization.java @@ -30,6 +30,7 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeFunction; /******************************************************************************* @@ -58,8 +59,16 @@ public class Memoization /******************************************************************************* + ** Get the memoized Value for a given input Key. ** + ** But note, this looks the same to the caller, whether the key just wasn't in + ** the internal map (e.g., had never been looked up), or if it was previously looked + ** up, and that returned null. In either case, the optional will be empty. + ** + ** See getMemoizedResult for where we can tell the difference (and we would + ** generally want to call that. *******************************************************************************/ + @Deprecated public Optional getResult(K key) { MemoizedResult result = map.get(key); @@ -77,7 +86,57 @@ public class Memoization /******************************************************************************* + ** Get the memoized Value for a given input Key - computing it if it wasn't previously + ** memoized (or expired). ** + ** In here, if the optional is empty, it means the value is null (whether that + ** came form memoization, or from the lookupFunction, you don't care - the answer + ** is null). + *******************************************************************************/ + public Optional getResult(K key, UnsafeFunction lookupFunction) + { + MemoizedResult result = map.get(key); + if(result != null) + { + if(result.getTime().isAfter(Instant.now().minus(timeout))) + { + ////////////////////////////////////////////////////////////////////////////// + // ok, we have a memoized value, and it's not expired, so we can return it. // + // of course, it might be a memoized null, so we use .ofNullable. // + ////////////////////////////////////////////////////////////////////////////// + return (Optional.ofNullable(result.getResult())); + } + } + + ///////////////////////////////////////////////////////////////////////////////////////////// + // ok - either we never memoized this key, or it's expired, so, apply the lookup function, // + // store the result, and then return the value (in an Optional.ofNullable) // + ///////////////////////////////////////////////////////////////////////////////////////////// + try + { + V value = lookupFunction.apply(key); + storeResult(key, value); + return (Optional.ofNullable(value)); + } + catch(Exception e) + { + LOG.warn("Uncaught Exception while executing a Memoization lookupFunction (to avoid this log, add a catch in the lookupFunction)", e); + storeResult(key, null); + return (Optional.empty()); + } + } + + + + /******************************************************************************* + ** Get a memoized result, optionally containing a Value, for a given input Key. + ** + ** In this method (contrasted with getResult), if the returned Optional is empty, + ** it means that we haven't ever looked up or memoized the key (or it's expired). + ** + ** If the returned Optional is not empty, then it means we've memoized something + ** (and it's not expired) - so if the Value from the MemoizedResult is null, + ** then null is the proper memoized value. *******************************************************************************/ public Optional> getMemoizedResult(K key) { @@ -86,7 +145,7 @@ public class Memoization { if(result.getTime().isAfter(Instant.now().minus(timeout))) { - return (Optional.ofNullable(result)); + return (Optional.of(result)); } } @@ -181,4 +240,47 @@ public class Memoization { return map; } + + + + /******************************************************************************* + ** Getter for timeout + *******************************************************************************/ + public Duration getTimeout() + { + return (this.timeout); + } + + + + /******************************************************************************* + ** Fluent setter for timeout + *******************************************************************************/ + public Memoization withTimeout(Duration timeout) + { + this.timeout = timeout; + return (this); + } + + + + /******************************************************************************* + ** Getter for maxSize + *******************************************************************************/ + public Integer getMaxSize() + { + return (this.maxSize); + } + + + + /******************************************************************************* + ** Fluent setter for maxSize + *******************************************************************************/ + public Memoization withMaxSize(Integer maxSize) + { + this.maxSize = maxSize; + return (this); + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/memoization/MemoizationTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/memoization/MemoizationTest.java index 85683482..27f60d0c 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/memoization/MemoizationTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/memoization/MemoizationTest.java @@ -32,11 +32,14 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import com.kingsrook.qqq.backend.core.BaseTest; import com.kingsrook.qqq.backend.core.utils.SleepUtils; +import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeFunction; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -122,6 +125,58 @@ class MemoizationTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testLookupFunction() + { + AtomicInteger lookupFunctionCallCounter = new AtomicInteger(0); + + Memoization memoization = new Memoization<>(); + + UnsafeFunction lookupFunction = numberString -> + { + lookupFunctionCallCounter.getAndIncrement(); + + if(numberString.equals("null")) + { + return (null); + } + + return Integer.parseInt(numberString); + }; + + ////////////////////////////////////////////////////////////////////////////////////////// + // get "1" twice - should return 1 each time, and call the lookup function exactly once // + ////////////////////////////////////////////////////////////////////////////////////////// + assertThat(memoization.getResult("1", lookupFunction)).isPresent().contains(1); + assertEquals(1, lookupFunctionCallCounter.get()); + + assertThat(memoization.getResult("1", lookupFunction)).isPresent().contains(1); + assertEquals(1, lookupFunctionCallCounter.get()); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////// + // now get "null" twice - should return null each time, and call the lookup function exactly once more // + ///////////////////////////////////////////////////////////////////////////////////////////////////////// + assertThat(memoization.getResult("null", lookupFunction)).isEmpty(); + assertEquals(2, lookupFunctionCallCounter.get()); + + assertThat(memoization.getResult("null", lookupFunction)).isEmpty(); + assertEquals(2, lookupFunctionCallCounter.get()); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // now make a call that throws twice - again, should return null each time, and only do one more loookup call // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + assertThat(memoization.getResult(null, lookupFunction)).isEmpty(); + assertEquals(3, lookupFunctionCallCounter.get()); + + assertThat(memoization.getResult(null, lookupFunction)).isEmpty(); + assertEquals(3, lookupFunctionCallCounter.get()); + } + + + /******************************************************************************* ** *******************************************************************************/