/*
* Copyright 2016 Martin Kouba
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.trimou.engine.cache;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.trimou.engine.cache.ComputingCache.Function;
import org.trimou.engine.cache.ComputingCache.Listener;
import org.trimou.engine.config.AbstractConfigurationAware;
import org.trimou.util.Checker;
import org.trimou.util.ImmutableMap;
import org.trimou.util.ImmutableMap.ImmutableMapBuilder;
/**
* A default computing cache factory producing computing cache implementations
* backed by {@link ConcurrentHashMap}.
*
* @author Martin Kouba
* @see Map#computeIfAbsent(Object, java.util.function.Function)
*/
public class DefaultComputingCacheFactory extends AbstractConfigurationAware
implements ComputingCacheFactory {
private final MaxSizeStrategy maxSizeStrategy;
/**
*
* @see MaxSizeStrategy#CLEAR
*/
public DefaultComputingCacheFactory() {
this(MaxSizeStrategy.CLEAR);
}
/**
*
* @param maxSizeStrategy
*/
public DefaultComputingCacheFactory(MaxSizeStrategy maxSizeStrategy) {
Checker.checkArgumentNotNull(maxSizeStrategy);
this.maxSizeStrategy = maxSizeStrategy;
}
@Override
public <K, V> ComputingCache<K, V> create(String consumerId,
Function<K, V> computingFunction, Long expirationTimeout,
Long maxSize, Listener<K> listener) {
if (maxSize != null && !maxSizeStrategy.isEvictionSupported()) {
throw new IllegalArgumentException(
"Max size limit not supported - use a different eviction strategy");
}
return new ConcurrentHashMapAdapter<>(computingFunction, maxSize,
maxSizeStrategy, expirationTimeout, listener);
}
/**
*
* @author Martin Kouba
*
* @param <K>
* @param <V>
*/
private static class ConcurrentHashMapAdapter<K, V>
implements ComputingCache<K, V> {
private static final Logger LOGGER = LoggerFactory
.getLogger(ConcurrentHashMapAdapter.class);
private final MaxSizeStrategy maxSizeStrategy;
private final Long maxSize;
private final Long expirationTimeout;
private final Listener<K> listener;
private final ConcurrentHashMap<K, CacheEntry<V>> map;
private final java.util.function.Function<K, CacheEntry<V>> computingFunctionAdapter;
/**
*
* @param computingFunction
* @param maxSize
* @param maxSizeStrategy
*/
ConcurrentHashMapAdapter(
ComputingCache.Function<K, V> computingFunction, Long maxSize,
MaxSizeStrategy maxSizeStrategy, Long expirationTimeout,
Listener<K> listener) {
this.map = new ConcurrentHashMap<>();
this.maxSize = maxSize;
this.expirationTimeout = expirationTimeout;
this.listener = listener;
this.computingFunctionAdapter = (key) -> {
if (maxSize != null && map.size() > maxSize) {
throw new MaxSizeExceededException();
}
return CacheEntry.of(computingFunction.compute(key));
};
this.maxSizeStrategy = maxSizeStrategy;
}
@Override
public V get(K key) {
CacheEntry<V> entry = compute(key);
if (entry.isExpired(expirationTimeout)) {
if (map.remove(key) != null) {
notifyListener(key, RemovalCause.EXPIRED);
}
entry = compute(key);
}
return entry.value;
}
@Override
public V getIfPresent(K key) {
CacheEntry<V> entry = map.get(key);
if (entry != null) {
if (entry.isExpired(expirationTimeout)) {
if (map.remove(key) != null) {
notifyListener(key, RemovalCause.EXPIRED);
}
} else {
return entry.value;
}
}
return null;
}
@Override
public void clear() {
map.clear();
}
@Override
public long size() {
return map.size();
}
@Override
public void invalidate(ComputingCache.KeyPredicate<K> predicate) {
for (Iterator<K> iterator = map.keySet().iterator(); iterator
.hasNext();) {
K key = iterator.next();
if (predicate.apply(key)) {
iterator.remove();
notifyListener(key, RemovalCause.INVALIDATE);
}
}
}
@Override
public Map<K, V> getAllPresent() {
ImmutableMapBuilder<K, V> builder = ImmutableMap.builder();
for (Entry<K, CacheEntry<V>> entry : map.entrySet()) {
if (!entry.getValue().isExpired(expirationTimeout)) {
builder.put(entry.getKey(), entry.getValue().value);
}
}
return builder.build();
}
private void notifyListener(K key, RemovalCause cause) {
if (listener != null) {
listener.entryInvalidated(key, cause.toString());
}
}
private CacheEntry<V> compute(K key) {
try {
return map.computeIfAbsent(key, computingFunctionAdapter);
} catch (MaxSizeExceededException e) {
handleMaxSizeExceeding();
// Theoretically, this may also throw MaxSizeExceededException
// if the limit is exceeded before the value is computed, which
// is unlikely.
return map.computeIfAbsent(key, computingFunctionAdapter);
}
}
private synchronized void handleMaxSizeExceeding() {
if (map.size() > maxSize) {
applyMaxSizeStrategy();
}
}
private void applyMaxSizeStrategy() {
switch (maxSizeStrategy) {
case CLEAR:
// Clearing the whole map is not quite elegant, but exceeding
// the
// limit should be an exteme situation
LOGGER.debug(
"Max size limit of {} exceeded - removing all entries from the cache",
maxSize);
if (listener != null) {
Set<K> keys = new HashSet<>(map.keySet());
map.clear();
for (K key : keys) {
notifyListener(key, RemovalCause.MAX_SIZE_EXCEEDED);
}
} else {
map.clear();
}
break;
default:
LOGGER.warn(
"Max size limit of {} exceeded but the eviction strategy {} is not implemented!",
maxSize, maxSizeStrategy);
break;
}
}
}
/**
* Defines the strategy applied when the max size limit is set and exceeded.
*
* @author Martin Kouba
*/
public enum MaxSizeStrategy {
/**
* Do nothing
*/
NOOP(false),
/**
* Remove all entries from the cache
*/
CLEAR(true),;
MaxSizeStrategy(boolean isEvictionSupported) {
this.isEvictionSupported = isEvictionSupported;
}
private boolean isEvictionSupported;
public boolean isEvictionSupported() {
return isEvictionSupported;
}
}
public enum RemovalCause {
EXPIRED,
MAX_SIZE_EXCEEDED,
INVALIDATE
}
/**
* Signals that the max size limit was exceeded.
*
* @author Martin Kouba
*/
private static class MaxSizeExceededException extends RuntimeException {
private static final long serialVersionUID = 1L;
}
private static class CacheEntry<T> {
private final Long createdAt;
private final T value;
static <T> CacheEntry<T> of(T value) {
return new CacheEntry<>(System.currentTimeMillis(), value);
}
private CacheEntry(Long createdTs, T value) {
this.createdAt = createdTs;
this.value = value;
}
boolean isExpired(Long expirationTimeout) {
return expirationTimeout != null && (System.currentTimeMillis()
- createdAt) >= expirationTimeout;
}
}
}