package org.molgenis.data.cache.l3;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.guava.CaffeinatedGuava;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import org.molgenis.data.Entity;
import org.molgenis.data.Fetch;
import org.molgenis.data.Query;
import org.molgenis.data.Repository;
import org.molgenis.data.support.QueryImpl;
import org.molgenis.data.transaction.DefaultMolgenisTransactionListener;
import org.molgenis.data.transaction.MolgenisTransactionManager;
import org.molgenis.data.transaction.TransactionInformation;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import javax.annotation.Nonnull;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentMap;
import static com.google.common.collect.Maps.newConcurrentMap;
import static java.util.Objects.requireNonNull;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.stream.Collectors.toList;
import static org.slf4j.LoggerFactory.getLogger;
/**
* In-memory Query cache containing Queries and resulting ids from cacheable repositories
*/
@Service
public class L3Cache extends DefaultMolgenisTransactionListener
{
private static final Logger LOG = getLogger(L3Cache.class);
private static final int MAX_CACHE_SIZE_PER_QUERY = 1000;
/**
* maps entity name to the loading cache with Query key and List of Identifiers
*/
private final ConcurrentMap<String, LoadingCache<Query<Entity>, List<Object>>> caches = newConcurrentMap();
private final TransactionInformation transactionInformation;
@Autowired
public L3Cache(MolgenisTransactionManager molgenisTransactionManager, TransactionInformation transactionInformation)
{
this.transactionInformation = requireNonNull(transactionInformation);
requireNonNull(molgenisTransactionManager).addTransactionListener(this);
}
@Override
public void afterCommitTransaction(String transactionId)
{
transactionInformation.getDirtyRepositories().forEach(caches::remove);
}
public List<Object> get(Repository<Entity> repository, Query<Entity> query)
{
// Set fetch to null because we are only caching identifiers
LoadingCache<Query<Entity>, List<Object>> cache = getQueryCache(repository);
Query<Entity> fetchlessQuery = new QueryImpl<>(query);
fetchlessQuery.setFetch(null);
return cache.getUnchecked(fetchlessQuery);
}
private LoadingCache<Query<Entity>, List<Object>> getQueryCache(Repository<Entity> repository)
{
String name = repository.getName();
if (!caches.containsKey(name))
{
caches.putIfAbsent(name, createQueryCache(repository));
}
return caches.get(name);
}
private LoadingCache<Query<Entity>, List<Object>> createQueryCache(Repository<Entity> repository)
{
LOG.trace("Creating Query cache for repository {}", repository.getName());
return CaffeinatedGuava.build(Caffeine.newBuilder().recordStats().maximumSize(MAX_CACHE_SIZE_PER_QUERY)
.expireAfterAccess(10, MINUTES), createCacheLoader(repository));
}
/**
* Create a cacheloader that loads entity ids from the repository and stores them together with their query
*
* @return the {@link CacheLoader}
*/
private CacheLoader<Query<Entity>, List<Object>> createCacheLoader(final Repository<Entity> repository)
{
String repositoryName = repository.getName();
Fetch idAttributeFetch = new Fetch().field(repository.getEntityType().getIdAttribute().getName());
return new CacheLoader<Query<Entity>, List<Object>>()
{
/**
* Loads {@link Entity} identifiers for a {@link Query}
* @param query the cache key to load
* @return {@link List} of identifier {@link Object}s
*/
@Override
public List<Object> load(@Nonnull Query<Entity> query)
{
LOG.trace("Loading identifiers from repository {} for query {}", repositoryName, query);
return repository.findAll(new QueryImpl<>(query).fetch(idAttributeFetch)).map(Entity::getIdValue)
.collect(toList());
}
};
}
/**
* Logs cumulative cache statistics for all known caches.
*/
@Scheduled(fixedRate = 60000)
public void logStatistics()
{
//TODO: do we want to log diff with last log instead?
if (LOG.isDebugEnabled())
{
LOG.debug("Cache stats:");
for (Map.Entry<String, LoadingCache<Query<Entity>, List<Object>>> cacheEntry : caches.entrySet())
{
LOG.debug("{}:{}", cacheEntry.getKey(), cacheEntry.getValue().stats());
}
}
}
}