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