mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2026-09-16 18:13:17 +00:00
ACS-11060 Use Guava in ParentAssocsCache (backport) (#3864)
This commit is contained in:
+4
-4
@@ -1234,7 +1234,7 @@
|
||||
"filename": "repository/src/main/resources/alfresco/repository.properties",
|
||||
"hashed_secret": "a4a747bd4ba5e3a5049cad116881867c71fb625b",
|
||||
"is_verified": false,
|
||||
"line_number": 245,
|
||||
"line_number": 249,
|
||||
"is_secret": false
|
||||
},
|
||||
{
|
||||
@@ -1242,7 +1242,7 @@
|
||||
"filename": "repository/src/main/resources/alfresco/repository.properties",
|
||||
"hashed_secret": "1459a56410378e4d3ab470eff570e5eae1742762",
|
||||
"is_verified": false,
|
||||
"line_number": 314,
|
||||
"line_number": 318,
|
||||
"is_secret": false
|
||||
},
|
||||
{
|
||||
@@ -1250,7 +1250,7 @@
|
||||
"filename": "repository/src/main/resources/alfresco/repository.properties",
|
||||
"hashed_secret": "84551ae5442affc9f1a2d3b4c86ae8b24860149d",
|
||||
"is_verified": false,
|
||||
"line_number": 773,
|
||||
"line_number": 777,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
@@ -1845,5 +1845,5 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"generated_at": "2025-12-10T12:15:02Z"
|
||||
"generated_at": "2026-02-25T12:24:46Z"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* #%L
|
||||
* Alfresco Repository
|
||||
* %%
|
||||
* Copyright (C) 2005 - 2023 Alfresco Software Limited
|
||||
* Copyright (C) 2005 - 2026 Alfresco Software Limited
|
||||
* %%
|
||||
* This file is part of the Alfresco software.
|
||||
* If the software was purchased under a paid Alfresco license, the terms of
|
||||
@@ -41,8 +41,6 @@ import java.util.Set;
|
||||
import java.util.SortedSet;
|
||||
import java.util.Stack;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@@ -188,7 +186,8 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO
|
||||
*/
|
||||
private ParentAssocsCache parentAssocsCache;
|
||||
private int parentAssocsCacheSize;
|
||||
private int parentAssocsCacheLimitFactor = 8;
|
||||
private int parentAssocsCacheLimitFactor;
|
||||
private int parentAssocsCacheConcurrencyLevel;
|
||||
|
||||
/**
|
||||
* Cache for fast lookups of child nodes by <b>cm:name</b>.
|
||||
@@ -399,6 +398,17 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO
|
||||
this.parentAssocsCacheLimitFactor = parentAssocsCacheLimitFactor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the concurrency level for the parent assocs cache.
|
||||
*
|
||||
* @param parentAssocsCacheConcurrencyLevel
|
||||
* the parentAssocsCacheConcurrencyLevel to set
|
||||
*/
|
||||
public void setParentAssocsCacheConcurrencyLevel(int parentAssocsCacheConcurrencyLevel)
|
||||
{
|
||||
this.parentAssocsCacheConcurrencyLevel = parentAssocsCacheConcurrencyLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the cache that maintains lookups by child <b>cm:name</b>
|
||||
*
|
||||
@@ -424,7 +434,7 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO
|
||||
PropertyCheck.mandatory(this, "usageDAO", usageDAO);
|
||||
|
||||
this.nodePropertyHelper = new NodePropertyHelper(dictionaryService, qnameDAO, localeDAO, contentDataDAO);
|
||||
this.parentAssocsCache = new ParentAssocsCache(this.parentAssocsCacheSize, this.parentAssocsCacheLimitFactor);
|
||||
this.parentAssocsCache = new ParentAssocsCache(this.parentAssocsCacheSize, this.parentAssocsCacheLimitFactor, this.parentAssocsCacheConcurrencyLevel);
|
||||
}
|
||||
|
||||
/* Cache helpers */
|
||||
@@ -4197,172 +4207,14 @@ public abstract class AbstractNodeDAOImpl implements NodeDAO, BatchingDAO
|
||||
// done
|
||||
}
|
||||
|
||||
/**
|
||||
* A Map-like class for storing ParentAssocsInfos. It prunes its oldest ParentAssocsInfo entries not only when a capacity is reached, but also when a total number of cached parents is reached, as this is what dictates the overall memory usage.
|
||||
*/
|
||||
private static class ParentAssocsCache
|
||||
{
|
||||
private final ReadWriteLock lock = new ReentrantReadWriteLock();
|
||||
private final int size;
|
||||
private final int maxParentCount;
|
||||
private final Map<Pair<Long, String>, ParentAssocsInfo> cache;
|
||||
private final Map<Pair<Long, String>, Pair<Long, String>> nextKeys;
|
||||
private final Map<Pair<Long, String>, Pair<Long, String>> previousKeys;
|
||||
private Pair<Long, String> firstKey;
|
||||
private Pair<Long, String> lastKey;
|
||||
private int parentCount;
|
||||
|
||||
/**
|
||||
* @param size
|
||||
* int
|
||||
* @param limitFactor
|
||||
* int
|
||||
*/
|
||||
public ParentAssocsCache(int size, int limitFactor)
|
||||
{
|
||||
this.size = size;
|
||||
this.maxParentCount = size * limitFactor;
|
||||
final int mapSize = size * 2;
|
||||
this.cache = new HashMap<Pair<Long, String>, ParentAssocsInfo>(mapSize);
|
||||
this.nextKeys = new HashMap<Pair<Long, String>, Pair<Long, String>>(mapSize);
|
||||
this.previousKeys = new HashMap<Pair<Long, String>, Pair<Long, String>>(mapSize);
|
||||
}
|
||||
|
||||
private ParentAssocsInfo get(Pair<Long, String> cacheKey)
|
||||
{
|
||||
lock.readLock().lock();
|
||||
try
|
||||
{
|
||||
return cache.get(cacheKey);
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void put(Pair<Long, String> cacheKey, ParentAssocsInfo parentAssocs)
|
||||
{
|
||||
lock.writeLock().lock();
|
||||
try
|
||||
{
|
||||
// If an entry already exists, remove it and do the necessary housekeeping
|
||||
if (cache.containsKey(cacheKey))
|
||||
{
|
||||
remove(cacheKey);
|
||||
}
|
||||
|
||||
// Add the value and prepend the key
|
||||
cache.put(cacheKey, parentAssocs);
|
||||
if (firstKey == null)
|
||||
{
|
||||
lastKey = cacheKey;
|
||||
}
|
||||
else
|
||||
{
|
||||
nextKeys.put(cacheKey, firstKey);
|
||||
previousKeys.put(firstKey, cacheKey);
|
||||
}
|
||||
firstKey = cacheKey;
|
||||
parentCount += parentAssocs.getParentAssocs().size();
|
||||
|
||||
// Now prune the oldest entries whilst we have more cache entries or cached parents than desired
|
||||
int currentSize = cache.size();
|
||||
while (currentSize > size || parentCount > maxParentCount)
|
||||
{
|
||||
remove(lastKey);
|
||||
currentSize--;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private ParentAssocsInfo remove(Pair<Long, String> cacheKey)
|
||||
{
|
||||
lock.writeLock().lock();
|
||||
try
|
||||
{
|
||||
// Remove from the map
|
||||
ParentAssocsInfo oldParentAssocs = cache.remove(cacheKey);
|
||||
|
||||
// If the object didn't exist, we are done
|
||||
if (oldParentAssocs == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Re-link the list
|
||||
Pair<Long, String> previousCacheKey = previousKeys.remove(cacheKey);
|
||||
Pair<Long, String> nextCacheKey = nextKeys.remove(cacheKey);
|
||||
if (nextCacheKey == null)
|
||||
{
|
||||
if (previousCacheKey == null)
|
||||
{
|
||||
firstKey = lastKey = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
lastKey = previousCacheKey;
|
||||
nextKeys.remove(previousCacheKey);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (previousCacheKey == null)
|
||||
{
|
||||
firstKey = nextCacheKey;
|
||||
previousKeys.remove(nextCacheKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
nextKeys.put(previousCacheKey, nextCacheKey);
|
||||
previousKeys.put(nextCacheKey, previousCacheKey);
|
||||
}
|
||||
}
|
||||
// Update the parent count
|
||||
parentCount -= oldParentAssocs.getParentAssocs().size();
|
||||
return oldParentAssocs;
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void clear()
|
||||
{
|
||||
lock.writeLock().lock();
|
||||
try
|
||||
{
|
||||
cache.clear();
|
||||
nextKeys.clear();
|
||||
previousKeys.clear();
|
||||
firstKey = lastKey = null;
|
||||
parentCount = 0;
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Returns a node's parent associations
|
||||
*/
|
||||
private ParentAssocsInfo getParentAssocsCached(Long nodeId)
|
||||
{
|
||||
Node node = getNodeNotNull(nodeId, false);
|
||||
Pair<Long, String> cacheKey = new Pair<Long, String>(nodeId, node.getTransaction().getChangeTxnId());
|
||||
ParentAssocsInfo value = parentAssocsCache.get(cacheKey);
|
||||
if (value == null)
|
||||
{
|
||||
value = loadParentAssocs(node.getNodeVersionKey());
|
||||
parentAssocsCache.put(cacheKey, value);
|
||||
}
|
||||
Pair<Long, String> cacheKey = new Pair<>(nodeId, node.getTransaction().getChangeTxnId());
|
||||
ParentAssocsInfo value = parentAssocsCache.get(cacheKey, () -> loadParentAssocs(node.getNodeVersionKey()));
|
||||
|
||||
// We have already validated on loading that we have a list in sync with the child node, so if the list is still
|
||||
// empty we have an integrity problem
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* #%L
|
||||
* Alfresco Repository
|
||||
* %%
|
||||
* Copyright (C) 2026 Alfresco Software Limited
|
||||
* %%
|
||||
* This file is part of the Alfresco software.
|
||||
* If the software was purchased under a paid Alfresco license, the terms of
|
||||
* the paid license agreement will prevail. Otherwise, the software is
|
||||
* provided under the following open source license terms:
|
||||
*
|
||||
* Alfresco is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Alfresco 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 Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.repo.domain.node;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
|
||||
import org.alfresco.error.AlfrescoRuntimeException;
|
||||
import org.alfresco.util.Pair;
|
||||
|
||||
/**
|
||||
* A Map-like class for storing ParentAssocsInfos, backed by a Google {@link Cache} implementation.
|
||||
*/
|
||||
class ParentAssocsCache
|
||||
{
|
||||
private final Cache<Pair<Long, String>, ParentAssocsInfo> cache;
|
||||
|
||||
/**
|
||||
* @param size
|
||||
* int
|
||||
* @param limitFactor
|
||||
* int
|
||||
* @param concurrencyLevel
|
||||
* int
|
||||
*/
|
||||
ParentAssocsCache(int size, int limitFactor, int concurrencyLevel)
|
||||
{
|
||||
final int maxParentCount = size * limitFactor;
|
||||
|
||||
this.cache = CacheBuilder.newBuilder()
|
||||
.maximumWeight(maxParentCount)
|
||||
.concurrencyLevel(concurrencyLevel)
|
||||
.weigher((Pair<Long, String> key, ParentAssocsInfo value) -> {
|
||||
var parentAssocsSize = Optional.ofNullable(value)
|
||||
.map(ParentAssocsInfo::getParentAssocs)
|
||||
.map(Map::size)
|
||||
.orElse(1);
|
||||
return Math.max(1, parentAssocsSize);
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
ParentAssocsInfo get(Pair<Long, String> cacheKey)
|
||||
{
|
||||
return cache.getIfPresent(cacheKey);
|
||||
}
|
||||
|
||||
ParentAssocsInfo get(Pair<Long, String> cacheKey, Callable<ParentAssocsInfo> valueLoader)
|
||||
{
|
||||
try
|
||||
{
|
||||
return cache.get(cacheKey, valueLoader);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new AlfrescoRuntimeException("Failed to load parent associations", e);
|
||||
}
|
||||
}
|
||||
|
||||
void put(Pair<Long, String> cacheKey, ParentAssocsInfo parentAssocs)
|
||||
{
|
||||
cache.put(cacheKey, parentAssocs);
|
||||
}
|
||||
|
||||
ParentAssocsInfo remove(Pair<Long, String> cacheKey)
|
||||
{
|
||||
ParentAssocsInfo old = cache.getIfPresent(cacheKey);
|
||||
if (old != null)
|
||||
{
|
||||
cache.invalidate(cacheKey);
|
||||
}
|
||||
return old;
|
||||
}
|
||||
|
||||
void clear()
|
||||
{
|
||||
cache.invalidateAll();
|
||||
}
|
||||
}
|
||||
@@ -138,6 +138,7 @@
|
||||
<property name="propertiesCache" ref="node.propertiesCache"/>
|
||||
<property name="parentAssocsCacheSize" value="${system.cache.parentAssocs.maxSize}"/>
|
||||
<property name="parentAssocsCacheLimitFactor" value="${system.cache.parentAssocs.limitFactor}"/>
|
||||
<property name="parentAssocsCacheConcurrencyLevel" value="${system.cache.parentAssocs.concurrencyLevel}"/>
|
||||
<property name="childByNameCache" ref="node.childByNameCache"/>
|
||||
<property name="cachingThreshold" value="${nodes.bulkLoad.cachingThreshold}"/>
|
||||
</bean>
|
||||
|
||||
@@ -147,6 +147,10 @@ system.cache.parentAssocs.maxSize=130000
|
||||
# memory usage.
|
||||
system.cache.parentAssocs.limitFactor=8
|
||||
|
||||
# Number of concurrent update threads.
|
||||
# Higher values reduce write contention but may increase memory usage.
|
||||
system.cache.parentAssocs.concurrencyLevel=4
|
||||
|
||||
#
|
||||
# Properties to limit resources spent on individual searches
|
||||
#
|
||||
|
||||
@@ -54,6 +54,7 @@ import org.alfresco.util.testing.category.NonBuildTests;
|
||||
|
||||
// From AppContext05TestSuite
|
||||
org.alfresco.repo.domain.node.NodeDAOTest.class,
|
||||
org.alfresco.repo.domain.node.ParentAssocsCacheTest.class,
|
||||
org.alfresco.repo.domain.subscriptions.SubscriptionDAOTest.class,
|
||||
org.alfresco.repo.security.permissions.impl.AclDaoComponentTest.class,
|
||||
org.alfresco.repo.domain.contentdata.ContentDataDAOTest.class,
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* #%L
|
||||
* Alfresco Repository
|
||||
* %%
|
||||
* Copyright (C) 2026 Alfresco Software Limited
|
||||
* %%
|
||||
* This file is part of the Alfresco software.
|
||||
* If the software was purchased under a paid Alfresco license, the terms of
|
||||
* the paid license agreement will prevail. Otherwise, the software is
|
||||
* provided under the following open source license terms:
|
||||
*
|
||||
* Alfresco is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Alfresco 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 Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.repo.domain.node;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
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 java.util.stream.IntStream;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.alfresco.error.AlfrescoRuntimeException;
|
||||
import org.alfresco.util.Pair;
|
||||
|
||||
public class ParentAssocsCacheTest
|
||||
{
|
||||
@Test
|
||||
public void testGetReturnsNullWhenAbsent()
|
||||
{
|
||||
ParentAssocsCache cache = new ParentAssocsCache(10, 2, 1);
|
||||
Pair<Long, String> key = new Pair<>(1L, "store");
|
||||
|
||||
assertNull(cache.get(key));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetWithLoaderCachesValue()
|
||||
{
|
||||
ParentAssocsCache cache = new ParentAssocsCache(10, 2, 1);
|
||||
Pair<Long, String> key = new Pair<>(2L, "store");
|
||||
ParentAssocsInfo info = parentAssocsInfo(10L, true);
|
||||
AtomicInteger calls = new AtomicInteger();
|
||||
Callable<ParentAssocsInfo> loader = () -> {
|
||||
calls.incrementAndGet();
|
||||
return info;
|
||||
};
|
||||
|
||||
ParentAssocsInfo first = cache.get(key, loader);
|
||||
ParentAssocsInfo second = cache.get(key, loader);
|
||||
|
||||
assertSame(info, first);
|
||||
assertSame(info, second);
|
||||
assertSame(info, cache.get(key));
|
||||
assertEquals(1, calls.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetWrapsExecutionException()
|
||||
{
|
||||
ParentAssocsCache cache = new ParentAssocsCache(10, 2, 1);
|
||||
Pair<Long, String> key = new Pair<>(3L, "store");
|
||||
RuntimeException root = new RuntimeException("boom");
|
||||
Callable<ParentAssocsInfo> loader = () -> {
|
||||
throw root;
|
||||
};
|
||||
|
||||
AlfrescoRuntimeException ex = assertThrows(AlfrescoRuntimeException.class, () -> cache.get(key, loader));
|
||||
|
||||
assertNotNull(ex.getCause());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPutAndRemoveReturnOld()
|
||||
{
|
||||
ParentAssocsCache cache = new ParentAssocsCache(10, 2, 1);
|
||||
Pair<Long, String> key = new Pair<>(4L, "store");
|
||||
ParentAssocsInfo info = parentAssocsInfo(20L, false);
|
||||
|
||||
cache.put(key, info);
|
||||
ParentAssocsInfo removed = cache.remove(key);
|
||||
|
||||
assertSame(info, removed);
|
||||
assertNull(cache.get(key));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClearInvalidatesAll()
|
||||
{
|
||||
ParentAssocsCache cache = new ParentAssocsCache(10, 2, 1);
|
||||
Pair<Long, String> key1 = new Pair<>(5L, "store");
|
||||
Pair<Long, String> key2 = new Pair<>(6L, "store");
|
||||
|
||||
cache.put(key1, parentAssocsInfo(30L, true));
|
||||
cache.put(key2, parentAssocsInfo(31L, false));
|
||||
cache.clear();
|
||||
|
||||
assertNull(cache.get(key1));
|
||||
assertNull(cache.get(key2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConcurrentGetWithLoaderOnlyLoadsOnce() throws Exception
|
||||
{
|
||||
ParentAssocsCache cache = new ParentAssocsCache(10, 2, 1);
|
||||
Pair<Long, String> key = new Pair<>(7L, "store");
|
||||
ParentAssocsInfo info = parentAssocsInfo(40L, true);
|
||||
AtomicInteger calls = new AtomicInteger();
|
||||
Callable<ParentAssocsInfo> loader = () -> {
|
||||
calls.incrementAndGet();
|
||||
return info;
|
||||
};
|
||||
int threads = 24;
|
||||
CountDownLatch start = new CountDownLatch(1);
|
||||
ExecutorService executor = Executors.newCachedThreadPool();
|
||||
try
|
||||
{
|
||||
List<Future<ParentAssocsInfo>> futures = new ArrayList<>(threads);
|
||||
for (int i = 0; i < threads; i++)
|
||||
{
|
||||
futures.add(executor.submit(() -> {
|
||||
start.await();
|
||||
return cache.get(key, loader);
|
||||
}));
|
||||
}
|
||||
start.countDown();
|
||||
for (Future<ParentAssocsInfo> future : futures)
|
||||
{
|
||||
assertSame(info, future.get(5, TimeUnit.SECONDS));
|
||||
}
|
||||
assertEquals(1, calls.get());
|
||||
}
|
||||
finally
|
||||
{
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConcurrentGetDifferentKeysLoadsEachOnce() throws Exception
|
||||
{
|
||||
ParentAssocsCache cache = new ParentAssocsCache(50, 2, 1);
|
||||
int keys = 16;
|
||||
List<Pair<Long, String>> keyList = IntStream.range(0, keys)
|
||||
.mapToObj(index -> new Pair<>((long) index + 100, "store"))
|
||||
.toList();
|
||||
List<AtomicInteger> counters = IntStream.range(0, keys)
|
||||
.mapToObj(index -> new AtomicInteger())
|
||||
.toList();
|
||||
CountDownLatch start = new CountDownLatch(1);
|
||||
ExecutorService executor = Executors.newCachedThreadPool();
|
||||
try
|
||||
{
|
||||
List<Future<ParentAssocsInfo>> futures = new ArrayList<>(keys);
|
||||
for (int i = 0; i < keys; i++)
|
||||
{
|
||||
int index = i;
|
||||
Pair<Long, String> key = keyList.get(i);
|
||||
ParentAssocsInfo info = parentAssocsInfo((long) index + 200, index % 2 == 0);
|
||||
Callable<ParentAssocsInfo> loader = () -> {
|
||||
counters.get(index).incrementAndGet();
|
||||
return info;
|
||||
};
|
||||
futures.add(executor.submit(() -> {
|
||||
start.await();
|
||||
return cache.get(key, loader);
|
||||
}));
|
||||
}
|
||||
start.countDown();
|
||||
for (int i = 0; i < keys; i++)
|
||||
{
|
||||
ParentAssocsInfo value = futures.get(i).get(5, TimeUnit.SECONDS);
|
||||
assertSame(cache.get(keyList.get(i)), value);
|
||||
}
|
||||
assertTrue(counters.stream().allMatch(counter -> counter.get() == 1));
|
||||
}
|
||||
finally
|
||||
{
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
private ParentAssocsInfo parentAssocsInfo(Long assocId, boolean isPrimary)
|
||||
{
|
||||
return new ParentAssocsInfo(false, false, parentAssocEntities(assocId, isPrimary));
|
||||
}
|
||||
|
||||
private List<ChildAssocEntity> parentAssocEntities(Long assocId, boolean isPrimary)
|
||||
{
|
||||
ChildAssocEntity assoc = new ChildAssocEntity();
|
||||
assoc.setId(assocId);
|
||||
assoc.setPrimary(isPrimary);
|
||||
return Arrays.asList(assoc);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user