/*
* Copyright 2012 Thomas Bocek
*
* 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 net.tomp2p.utils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A map with expiration and more or less LRU. Since the maps are separated in segments, the LRU is done for each
* segment. A segment is chosen based on the hash of the key. If one segments is more loaded than another, then an entry
* of the loaded segment may get evicted before an entry used least recently from an other segment. The expiration is
* done best effort. There is no thread checking for timed out entries since the cache has a fixed size. Once an entry
* times out, it remains in the map until it either is accessed or evicted. A test showed that for the default entry
* size of 1024, this map has a size of 967 if 1024 items are inserted. This is due to the segmentation and hashing.
*
* @author Thomas Bocek
* @param <K>
* the type of the key
* @param <V>
* the type of the value
*/
public class ConcurrentCacheMap<K, V> implements ConcurrentMap<K, V> {
private static final Logger LOGGER = LoggerFactory.getLogger(ConcurrentCacheMap.class);
/**
* Number of segments that can be accessed concurrently.
*/
public static final int SEGMENT_NR = 16;
/**
* Max. number of entries that the map can hold until the least recently used gets replaced
*/
public static final int MAX_ENTRIES = 1024;
/**
* Time to live for a value. The value may stay longer in the map, but it is considered invalid.
*/
public static final int DEFAULT_TIME_TO_LIVE = 60;
private final CacheMap<K, ExpiringObject>[] segments;
private final int timeToLive;
private final boolean refreshTimeout;
private final AtomicInteger removedCounter = new AtomicInteger();
/**
* Creates a new instance of ConcurrentCacheMap using the supplied values and a {@link CacheMap} for the internal
* data structure.
*/
public ConcurrentCacheMap() {
this(DEFAULT_TIME_TO_LIVE, MAX_ENTRIES, true);
}
/**
* Creates a new instance of ConcurrentCacheMap using the supplied values and a {@link CacheMap} for the internal
* data structure.
*
* @param timeToLive
* The time-to-live value (seconds)
* @param maxEntries
* Set the maximum number of entries until items gets replaced with LRU
*/
public ConcurrentCacheMap(final int timeToLive, final int maxEntries) {
this(timeToLive, maxEntries, true);
}
/**
* Creates a new instance of ConcurrentCacheMap using the supplied values and a {@link CacheMap} for the internal
* data structure.
*
* @param timeToLive
* The time-to-live value (seconds)
* @param maxEntries
* The maximum entries to keep in cache, default is 1024
* @param refreshTimeout
* If set to true, timeout will be reset in case of {@link #putIfAbsent(Object, Object)}
*/
@SuppressWarnings("unchecked")
public ConcurrentCacheMap(final int timeToLive, final int maxEntries, final boolean refreshTimeout) {
this.segments = new CacheMap[SEGMENT_NR];
final int maxEntriesPerSegment = maxEntries / SEGMENT_NR;
for (int i = 0; i < SEGMENT_NR; i++) {
// set the cachemap to true, since it should behave as a regular map
segments[i] = new CacheMap<K, ExpiringObject>(maxEntriesPerSegment, true);
}
this.timeToLive = timeToLive;
this.refreshTimeout = refreshTimeout;
}
/**
* Returns the segment based on the key.
*
* @param key
* The key where the hash code identifies the segment
* @return The cache map that corresponds to this segment
*/
private CacheMap<K, ExpiringObject> segment(final Object key) {
return segments[(key.hashCode() & Integer.MAX_VALUE) % SEGMENT_NR];
}
@Override
public V put(final K key, final V value) {
final ExpiringObject newValue = new ExpiringObject(value, System.currentTimeMillis());
final CacheMap<K, ExpiringObject> segment = segment(key);
ExpiringObject oldValue;
synchronized (segment) {
oldValue = segment.put(key, newValue);
}
if (oldValue == null || oldValue.isExpired()) {
return null;
}
return oldValue.getValue();
}
@Override
/**
* This does not reset the timer!
*/
public V putIfAbsent(final K key, final V value) {
final CacheMap<K, ExpiringObject> segment = segment(key);
final ExpiringObject newValue = new ExpiringObject(value, System.currentTimeMillis());
ExpiringObject oldValue = null;
synchronized (segment) {
if (!segment.containsKey(key)) {
oldValue = segment.put(key, newValue);
} else {
oldValue = segment.get(key);
if (oldValue.isExpired()) {
segment.put(key, newValue);
} else if (refreshTimeout) {
oldValue = new ExpiringObject(oldValue.getValue(), System.currentTimeMillis());
segment.put(key, oldValue);
}
}
}
if (oldValue == null || oldValue.isExpired()) {
return null;
}
return oldValue.getValue();
}
@SuppressWarnings("unchecked")
@Override
public V get(final Object key) {
final CacheMap<K, ExpiringObject> segment = segment(key);
final ExpiringObject oldValue;
synchronized (segment) {
oldValue = segment.get(key);
}
if (oldValue != null) {
if (expire(segment, (K) key, oldValue)) {
return null;
} else {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("get: " + key + ";" + oldValue.getValue());
}
return oldValue.getValue();
}
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("get not found: " + key);
}
return null;
}
@Override
public V remove(final Object key) {
final CacheMap<K, ExpiringObject> segment = segment(key);
final ExpiringObject oldValue;
synchronized (segment) {
oldValue = segment.remove(key);
}
if (oldValue == null || oldValue.isExpired()) {
return null;
}
return oldValue.getValue();
}
@SuppressWarnings("unchecked")
@Override
public boolean remove(final Object key, final Object value) {
final CacheMap<K, ExpiringObject> segment = segment(key);
final ExpiringObject oldValue;
boolean removed = false;
synchronized (segment) {
oldValue = segment.get(key);
if (oldValue != null && oldValue.equals(value) && !oldValue.isExpired()) {
removed = segment.remove(key) != null;
}
}
if (oldValue != null) {
expire(segment, (K) key, oldValue);
}
return removed;
}
@SuppressWarnings("unchecked")
@Override
public boolean containsKey(final Object key) {
final CacheMap<K, ExpiringObject> segment = segment(key);
final ExpiringObject oldValue;
synchronized (segment) {
oldValue = segment.get(key);
}
if (oldValue != null) {
if (!expire(segment, (K) key, oldValue)) {
return true;
}
}
return false;
}
@Override
public boolean containsValue(final Object value) {
for (final CacheMap<K, ExpiringObject> segment : segments) {
synchronized (segment) {
expireSegment(segment);
if (segment.containsValue(value)) {
return true;
}
}
}
return false;
}
@Override
public int size() {
int size = 0;
for (final CacheMap<K, ExpiringObject> segment : segments) {
synchronized (segment) {
expireSegment(segment);
size += segment.size();
}
}
return size;
}
@Override
public boolean isEmpty() {
for (final CacheMap<K, ExpiringObject> segment : segments) {
synchronized (segment) {
expireSegment(segment);
if (!segment.isEmpty()) {
return false;
}
}
}
return true;
}
@Override
public void clear() {
for (final CacheMap<K, ExpiringObject> segment : segments) {
synchronized (segment) {
segment.clear();
}
}
}
@Override
public int hashCode() {
int hashCode = 0;
for (final CacheMap<K, ExpiringObject> segment : segments) {
synchronized (segment) {
expireSegment(segment);
// as seen in AbstractMap
hashCode += segment.hashCode();
}
}
return hashCode;
}
@Override
public Set<K> keySet() {
final Set<K> retVal = new HashSet<K>();
for (final CacheMap<K, ExpiringObject> segment : segments) {
synchronized (segment) {
expireSegment(segment);
retVal.addAll(segment.keySet());
}
}
return retVal;
}
@Override
public void putAll(final Map<? extends K, ? extends V> inMap) {
for (final Entry<? extends K, ? extends V> e : inMap.entrySet()) {
this.put(e.getKey(), e.getValue());
}
}
@Override
public Collection<V> values() {
final Collection<V> retVal = new ArrayList<V>();
for (final CacheMap<K, ExpiringObject> segment : segments) {
synchronized (segment) {
final Iterator<ExpiringObject> iterator = segment.values().iterator();
while (iterator.hasNext()) {
final ExpiringObject expiringObject = iterator.next();
if (expiringObject.isExpired()) {
iterator.remove();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("remove in entrySet " + expiringObject.getValue());
}
removedCounter.incrementAndGet();
} else {
retVal.add(expiringObject.getValue());
}
}
}
}
return retVal;
}
@Override
public Set<Map.Entry<K, V>> entrySet() {
final Set<Map.Entry<K, V>> retVal = new HashSet<Map.Entry<K, V>>();
for (final CacheMap<K, ExpiringObject> segment : segments) {
synchronized (segment) {
final Iterator<Map.Entry<K, ExpiringObject>> iterator = segment.entrySet().iterator();
while (iterator.hasNext()) {
final Map.Entry<K, ExpiringObject> entry = iterator.next();
if (entry.getValue().isExpired()) {
iterator.remove();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("remove in entrySet " + entry.getValue().getValue());
}
removedCounter.incrementAndGet();
} else {
retVal.add(new Map.Entry<K, V>() {
@Override
public K getKey() {
return entry.getKey();
}
@Override
public V getValue() {
return entry.getValue().getValue();
}
@Override
public V setValue(final V value) {
throw new UnsupportedOperationException("not supported");
}
});
}
}
}
}
return retVal;
}
@Override
public boolean replace(final K key, final V oldValue, final V newValue) {
final ExpiringObject oldValue2 = new ExpiringObject(oldValue, 0L);
final ExpiringObject newValue2 = new ExpiringObject(newValue, System.currentTimeMillis());
final CacheMap<K, ExpiringObject> segment = segment(key);
final ExpiringObject oldValue3;
boolean replaced = false;
synchronized (segment) {
oldValue3 = segment.get(key);
if (oldValue3 != null && !oldValue3.isExpired() && oldValue2.equals(oldValue3.getValue())) {
segment.put(key, newValue2);
replaced = true;
}
}
if (oldValue3 != null) {
expire(segment, key, oldValue3);
}
return replaced;
}
@Override
public V replace(final K key, final V value) {
final ExpiringObject newValue = new ExpiringObject(value, System.currentTimeMillis());
final CacheMap<K, ExpiringObject> segment = segment(key);
final ExpiringObject oldValue;
synchronized (segment) {
oldValue = segment.get(key);
if (oldValue != null && !oldValue.isExpired()) {
segment.put(key, newValue);
}
}
if (oldValue == null) {
return null;
}
if (expire(segment, key, oldValue)) {
return null;
}
return oldValue.getValue();
}
/**
* Expires a key in a segment. If a key value pair is expired, it will get removed.
*
* @param segment
* The segment
* @param key
* The key
* @param value
* The value
* @return True if expired, otherwise false.
*/
private boolean expire(final CacheMap<K, ExpiringObject> segment, final K key, final ExpiringObject value) {
if (value.isExpired()) {
synchronized (segment) {
final ExpiringObject tmp = segment.get(key);
if (tmp != null && tmp.equals(value)) {
segment.remove(key);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("remove in expire " + value.getValue());
}
removedCounter.incrementAndGet();
}
}
return true;
}
return false;
}
/**
* Fast expiration. Since the ExpiringObject is ordered the for loop can break early if a object is not expired.
*
* @param segment
* The segment
*/
private void expireSegment(final CacheMap<K, ExpiringObject> segment) {
final Iterator<ExpiringObject> iterator = segment.values().iterator();
while (iterator.hasNext()) {
final ExpiringObject expiringObject = iterator.next();
if (expiringObject.isExpired()) {
iterator.remove();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("remove in expireAll " + expiringObject.getValue());
}
removedCounter.incrementAndGet();
} else {
break;
}
}
}
/**
* @return The number of expired objects
*/
public int expiredCounter() {
return removedCounter.get();
}
/**
* An object that also holds expriation information.
*/
private class ExpiringObject {
private final V value;
private final long lastAccessTime;
private static final int MS_IN_S = 1000;
/**
* Creates a new expiring object with the given time of access.
*
* @param value
* The value that is wrapped in this class
* @param lastAccessTime
* The time of access
*/
ExpiringObject(final V value, final long lastAccessTime) {
if (value == null) {
throw new IllegalArgumentException("An expiring object cannot be null.");
}
this.value = value;
this.lastAccessTime = lastAccessTime;
}
/**
* @return If entry is expired
*/
public boolean isExpired() {
return System.currentTimeMillis() >= lastAccessTime + (timeToLive * MS_IN_S);
}
/**
* @return The wrapped value
*/
public V getValue() {
return value;
}
@Override
public boolean equals(final Object obj) {
if (!(obj instanceof ConcurrentCacheMap.ExpiringObject)) {
return false;
}
@SuppressWarnings("unchecked")
final ExpiringObject exp = (ExpiringObject) obj;
return value.equals(exp.value);
}
@Override
public int hashCode() {
return value.hashCode();
}
}
}