Add Memoization class and supports

This commit is contained in:
2023-10-16 15:10:30 -05:00
parent e633ea8ed1
commit e2859aeb89
3 changed files with 361 additions and 0 deletions

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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");
}
}