mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 13:10:44 +00:00
Add Memoization class and supports
This commit is contained in:
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<K, V>
|
||||||
|
{
|
||||||
|
private static final QLogger LOG = QLogger.getLogger(Memoization.class);
|
||||||
|
|
||||||
|
private final Map<K, MemoizedResult<V>> map = Collections.synchronizedMap(new LinkedHashMap<>());
|
||||||
|
|
||||||
|
private Duration timeout = Duration.ofSeconds(600);
|
||||||
|
private Integer maxSize = 1000;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Constructor
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public Memoization()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public Optional<V> getResult(K key)
|
||||||
|
{
|
||||||
|
MemoizedResult<V> 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<Map.Entry<K, MemoizedResult<V>>> 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<K, MemoizedResult<V>> getMap()
|
||||||
|
{
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<T>
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<String, Integer> 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<String, Integer> memoization = new Memoization<>();
|
||||||
|
ExecutorService executorService = Executors.newFixedThreadPool(20);
|
||||||
|
|
||||||
|
List<Future<?>> 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<Future<?>> iterator = futures.iterator();
|
||||||
|
while(iterator.hasNext())
|
||||||
|
{
|
||||||
|
Future<?> next = iterator.next();
|
||||||
|
if(next.isDone())
|
||||||
|
{
|
||||||
|
Object o = next.get();
|
||||||
|
iterator.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("All Done");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Reference in New Issue
Block a user