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 new file mode 100644 index 00000000..df385bd7 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/memoization/Memoization.java @@ -0,0 +1,155 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.utils.memoization; + + +import java.time.Duration; +import java.time.Instant; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import com.kingsrook.qqq.backend.core.logging.QLogger; + + +/******************************************************************************* + ** Basic memoization functionality - with result timeouts (only when doing a get - + ** there's no cleanup thread), and max-size. + *******************************************************************************/ +public class Memoization +{ + private static final QLogger LOG = QLogger.getLogger(Memoization.class); + + private final Map> map = Collections.synchronizedMap(new LinkedHashMap<>()); + + private Duration timeout = Duration.ofSeconds(600); + private Integer maxSize = 1000; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public Memoization() + { + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Optional getResult(K key) + { + MemoizedResult result = map.get(key); + if(result != null) + { + if(result.getTime().isAfter(Instant.now().minus(timeout))) + { + return (Optional.of(result.getResult())); + } + } + + return (Optional.empty()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void storeResult(K key, V value) + { + map.put(key, new MemoizedResult<>(value)); + + ////////////////////////////////////// + // make sure map didn't get too big // + // do this thread safely, please // + ////////////////////////////////////// + try + { + if(map.size() > maxSize) + { + synchronized(map) + { + Iterator>> iterator = null; + while(map.size() > maxSize) + { + if(iterator == null) + { + iterator = map.entrySet().iterator(); + } + + if(iterator.hasNext()) + { + iterator.next(); + iterator.remove(); + } + else + { + break; + } + } + } + } + } + catch(Exception e) + { + LOG.error("Error managing size of a Memoization", e); + } + } + + + + /******************************************************************************* + ** Setter for timeoutSeconds + ** + *******************************************************************************/ + public void setTimeout(Duration timeout) + { + this.timeout = timeout; + } + + + + /******************************************************************************* + ** Setter for maxSize + ** + *******************************************************************************/ + public void setMaxSize(Integer maxSize) + { + this.maxSize = maxSize; + } + + + + /******************************************************************************* + ** package-private - for tests to look at the map. + ** + *******************************************************************************/ + Map> getMap() + { + return map; + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/memoization/MemoizedResult.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/memoization/MemoizedResult.java new file mode 100644 index 00000000..ba50211e --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/memoization/MemoizedResult.java @@ -0,0 +1,70 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.utils.memoization; + + +import java.time.Instant; + + +/******************************************************************************* + ** Object stored in the Memoization class. Shouldn't need to be visible outside + ** its package. + *******************************************************************************/ +class MemoizedResult +{ + private T result; + private Instant time; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public MemoizedResult(T result) + { + this.result = result; + this.time = Instant.now(); + } + + + + /******************************************************************************* + ** Getter for result + ** + *******************************************************************************/ + public T getResult() + { + return result; + } + + + + /******************************************************************************* + ** Getter for time + ** + *******************************************************************************/ + public Instant getTime() + { + return time; + } +} 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 new file mode 100644 index 00000000..c6858168 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/memoization/MemoizationTest.java @@ -0,0 +1,136 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.utils.memoization; + + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.utils.SleepUtils; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + + +/******************************************************************************* + ** Unit test for Memoization + *******************************************************************************/ +class MemoizationTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() + { + Memoization memoization = new Memoization<>(); + memoization.setMaxSize(3); + memoization.setTimeout(Duration.ofMillis(100)); + + assertThat(memoization.getResult("one")).isEmpty(); + memoization.storeResult("one", 1); + assertThat(memoization.getResult("one")).isPresent().get().isEqualTo(1); + + //////////////////////////////////////////////////// + // store 3 more results - this should force 1 out // + //////////////////////////////////////////////////// + memoization.storeResult("two", 2); + memoization.storeResult("three", 3); + memoization.storeResult("four", 4); + assertThat(memoization.getResult("one")).isEmpty(); + + ////////////////////////////////// + // make sure others are present // + ////////////////////////////////// + assertThat(memoization.getResult("two")).isPresent().get().isEqualTo(2); + assertThat(memoization.getResult("three")).isPresent().get().isEqualTo(3); + assertThat(memoization.getResult("four")).isPresent().get().isEqualTo(4); + + ///////////////////////////////////////////////////////////// + // wait more than the timeout, then make sure all are gone // + ///////////////////////////////////////////////////////////// + SleepUtils.sleep(150, TimeUnit.MILLISECONDS); + assertThat(memoization.getResult("two")).isEmpty(); + assertThat(memoization.getResult("three")).isEmpty(); + assertThat(memoization.getResult("four")).isEmpty(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + @Disabled("Slow, so not for CI - but good to demonstrate thread-safety during dev") + void testMultiThread() throws InterruptedException, ExecutionException + { + Memoization memoization = new Memoization<>(); + ExecutorService executorService = Executors.newFixedThreadPool(20); + + List> futures = new ArrayList<>(); + + for(int i = 0; i < 20; i++) + { + int finalI = i; + futures.add(executorService.submit(() -> + { + System.out.println("Start " + finalI); + for(int n = 0; n < 1_000_000; n++) + { + memoization.storeResult(String.valueOf(n), n); + memoization.getResult(String.valueOf(n)); + + if(n % 100_000 == 0) + { + System.out.format("Thread %d at %,d\n", finalI, +n); + } + } + System.out.println("End " + finalI); + })); + } + + while(!futures.isEmpty()) + { + Iterator> iterator = futures.iterator(); + while(iterator.hasNext()) + { + Future next = iterator.next(); + if(next.isDone()) + { + Object o = next.get(); + iterator.remove(); + } + } + } + + System.out.println("All Done"); + } + +} \ No newline at end of file