Commit cf899455 authored by Tigran Mkrtchyan's avatar Tigran Mkrtchyan
Browse files

utility: add generic cache implementation

There are several usecases where we need a Map where
entries orphan entries are removed. The usual use case
the cleanup of disconnected sessions.
parent efa6809c
/*
* This library is free software; you can redistribute it and/or modify
* it under the terms of the GNU Library General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This library 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 Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this program (see the file COPYING.LIB for more
* details); if not, write to the Free Software Foundation, Inc.,
* 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package org.dcache.utils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A Dictionary where value associated with the key may become unavailable due
* to validity timeout.
*
* Typical usage is:
* <pre>
* Cache cache = new Cache<String, String>("test cache", 10, TimeUnit.HOURS.toMillis(1),
* TimeUnit.MINUTES.toMillis(5));
*
* cache.put("key", "value");
* String value = cache.get("key");
* if( value == null ) {
* System.out.println("entry expired");
* }
*
* </pre>
* @author Tigran Mkrtchyan
* @param <K> the type of keys maintained by this cache
* @param <V> the type of cached values
*/
public class Cache<K, V> extends TimerTask {
private static final Logger _log = Logger.getLogger(Cache.class.getName());
/**
* {@link TimerTask} to periodically check and remove expired entries.
*/
@Override
public void run() {
List<V> expiredEntries = new ArrayList<V>();
_accessLock.lock();
try {
long now = System.currentTimeMillis();
Iterator<Map.Entry<K, CacheElement<V>>> entries = _storage.entrySet().iterator();
while(entries.hasNext()) {
Map.Entry<K, CacheElement<V>> entry = entries.next();
CacheElement<V> cacheElement = entry.getValue();
if (!cacheElement.validAt(now)) {
_log.log(Level.FINEST, "Cleaning expired entry key = [{0}], value = [{1}]",
new Object[]{entry.getKey(), cacheElement.getObject()});
entries.remove();
expiredEntries.add(cacheElement.getObject());
}
}
} finally {
_accessLock.unlock();
}
for (V v : expiredEntries) {
_eventListener.notifyExpired(this, v);
}
}
/**
* The name of this cache.
*/
private final String _name;
/**
* Maximum allowed time, in milliseconds, that an object is allowed to be cached.
* After expiration of this time cache entry invalidated.
*/
private final long _defaultEntryMaxLifeTime;
/**
* Time in milliseconds since last use of the object. After expiration of this
* time cache entry is invalidated.
*/
private final long _defaultEntryIdleTime;
/**
* Maximum number of entries in cache.
*/
private final int _size;
/**
* The storage.
*/
private final Map<K, CacheElement<V>> _storage;
/**
* 'Expire threads' used to detect and remove expired entries.
*/
private final Timer _cleaner = new Timer();
/**
* Internal storage access lock.
*/
private final Lock _accessLock = new ReentrantLock();
/**
* Cache event listener.
*/
private final CacheEventListener<K, V> _eventListener;
/**
* Create new cache instance with default {@link CacheEventListener} and
* default cleanup period.
*
* @param name Unique id for this cache.
* @param size maximal number of elements.
* @param default entryLifeTime maximal time in milliseconds.
* @param default entryIdleTime maximal idle time in milliseconds.
*/
public Cache(String name, int size, long entryLifeTime, long entryIdleTime) {
this(name, size, entryLifeTime, entryIdleTime,
new NopCacheEventListener<K, V>(),
30, TimeUnit.SECONDS);
}
/**
* Create new cache instance.
*
* @param name Unique id for this cache.
* @param size maximal number of elements.
* @param default entryLifeTime maximal time in milliseconds.
* @param default entryIdleTime maximal idle time in milliseconds.
* @param eventListener {@link CacheEventListener}
* @param timeValue how oftem cleaner thread have to check for invalidated entries.
* @param timeUnit a {@link TimeUnit} determining how to interpret the
* <code>timeValue</code> parameter.
*/
public Cache(String name, int size, long entryLifeTime, long entryIdleTime,
CacheEventListener<K, V> eventListener, long timeValue, TimeUnit timeUnit) {
_name = name;
_size = size;
_defaultEntryMaxLifeTime = entryLifeTime;
_defaultEntryIdleTime = entryIdleTime;
_storage = new HashMap<K, CacheElement<V>>(_size);
_eventListener = eventListener;
_cleaner.schedule(this, 0, timeUnit.toMillis(timeValue));
}
/**
* Get cache's name.
* @return name of the cache.
*/
public String getName() {
return _name;
}
/**
* Put/Update cache entry.
*
* @param k key associated with the value.
* @param v value associated with key.
*
* @throws MissingResourceException if Cache limit is reached.
*/
public void put(K k, V v) {
this.put(k, v, _defaultEntryMaxLifeTime, _defaultEntryIdleTime);
}
/**
* Put/Update cache entry.
*
* @param k key associated with the value.
* @param v value associated with key.
* @param entryMaxLifeTime maximal life time in milliseconds.
* @param entryIdleTime maximal idel time in milliseconds.
*
* @throws MissingResourceException if Cache limit is reached.
*/
public void put(K k, V v, long entryMaxLifeTime, long entryIdleTime) {
_log.log(Level.FINEST, "Adding new cache entry: key = [{0}], value = [{1}]",
new Object[]{k, v});
_accessLock.lock();
try {
if( _storage.size() >= _size && !_storage.containsKey(k)) {
_log.log(Level.INFO, "Cache limit reached: {0}", _size);
throw new MissingResourceException("Cache limit reached", Cache.class.getName(), "");
}
_storage.put(k, new CacheElement<V>(v, entryMaxLifeTime, entryIdleTime));
} finally {
_accessLock.unlock();
}
_eventListener.notifyPut(this, v);
}
/**
* Get stored value. If {@link Cache} does not have the associated entry or
* entry live time is expired <code>null</code> is returned.
* @param k key associated with entry.
* @return cached value associated with specified key.
*/
public V get(K k) {
V v;
boolean valid;
_accessLock.lock();
try {
CacheElement<V> element = _storage.get(k);
if (element == null) {
_log.log(Level.FINEST, "No cache hits for key = [{0}]", k);
return null;
}
long now = System.currentTimeMillis();
valid = element.validAt(now);
v = element.getObject();
if ( !valid ) {
_log.log(Level.FINEST, "Cache hits but entry expired for key = [{0}], value = [{1}]",
new Object[]{k, v});
_storage.remove(k);
} else {
_log.log(Level.FINEST, "Cache hits for key = [{0}], value = [{1}]",
new Object[]{k, v});
}
} finally {
_accessLock.unlock();
}
if(!valid) {
_eventListener.notifyRemove(this, v);
v = null;
}else{
_eventListener.notifyGet(this, v);
}
return v;
}
/**
* Remove entry associated with key.
*
* @param k key
* @return true if existing valid entry was removed.
*/
public boolean remove(K k) {
V v;
boolean valid;
_accessLock.lock();
try {
CacheElement<V> element = _storage.remove(k);
if( element == null ) return false;
valid = element.validAt(System.currentTimeMillis());
v = element.getObject();
} finally {
_accessLock.unlock();
}
_log.log(Level.FINEST, "Removing entry: active = [{0}] key = [{1}], value = [{2}]",
new Object[]{valid, k, v});
_eventListener.notifyRemove(this, v);
return valid;
}
/**
* Get number of elements inside the cache.
*
* @return number of elements.
*/
int size() {
_accessLock.lock();
try {
return _storage.size();
} finally {
_accessLock.unlock();
}
}
/**
* Get maximal idle time until entry become unavailable.
*
* @return time in milliseconds.
*/
public long getEntryIdleTime() {
return _defaultEntryIdleTime;
}
/**
* Get maximal total time until entry become unavailable.
*
* @return time in milliseconds.
*/
public long getEntryLiveTime() {
return _defaultEntryMaxLifeTime;
}
/**
* Remove all values from the Cache. Notice, that remove notifications are not triggered.
*/
public void clear() {
_log.log(Level.FINEST, "Cleaning the cache");
_accessLock.lock();
try {
_storage.clear();
} finally {
_accessLock.unlock();
}
}
/**
* Get {@link List<V>} of entries.
* @return list of entries.
*/
public List<V> entries() {
List<V> entries;
_accessLock.lock();
try {
entries = new ArrayList<V>(_storage.size());
for(CacheElement<V> e: _storage.values()) {
entries.add(e.getObject());
}
} finally {
_accessLock.unlock();
}
return entries;
}
}
/*
* This library is free software; you can redistribute it and/or modify
* it under the terms of the GNU Library General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This library 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 Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this program (see the file COPYING.LIB for more
* details); if not, write to the Free Software Foundation, Inc.,
* 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package org.dcache.utils;
import java.util.Date;
/**
* CacheElement wrapper.
*
* Keeps track elements creation and last access time.
* @param <V>
*/
public class CacheElement<V> {
/**
* Maximum allowed time, in milliseconds, that an object is allowed to be cached.
* After expiration of this time cache entry invalidated.
*/
private final long _maxLifeTime;
/**
* Time in milliseconds since last use of the object. After expiration of this
* time cache entry is invalidated.
*/
private final long _idleTime;
/**
* Element creation time.
*/
private final long _creationTime = System.currentTimeMillis();
/**
* Elements last access time.
*/
private long _lastAccessTime = _creationTime;
/**
* internal object.
*/
private final V _inner;
CacheElement(V inner, long maxLifeTime, long idleTime) {
_inner = inner;
_maxLifeTime = maxLifeTime;
_idleTime = idleTime;
}
/**
* Get internal object stored in this element.
* This operation will update this element's last access time with the current time.
*
* @return internal object.
*/
V getObject() {
_lastAccessTime = System.currentTimeMillis();
return _inner;
}
/**
* Check the entry's validity at the specified time.
*
* @param time in milliseconds since 1 of January 1970.
*
* @return true if entry still valid and false otherwise.
*/
boolean validAt(long time) {
return time - _lastAccessTime < _idleTime && time - _creationTime < _maxLifeTime;
}
@Override
public String toString() {
long now = System.currentTimeMillis();
String s = String.format("Element: [%s], created: %s, last access: %s, life time %d, idle: %d, max idle: %d",
_inner.toString(), new Date( _creationTime), new Date(_lastAccessTime),
_maxLifeTime, now - _lastAccessTime, _idleTime);
return s;
}
}
/*
* This library is free software; you can redistribute it and/or modify
* it under the terms of the GNU Library General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This library 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 Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this program (see the file COPYING.LIB for more
* details); if not, write to the Free Software Foundation, Inc.,
* 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package org.dcache.utils;
/**
* Cache event notification. Reacts on:
* <pre>
* <code>put</code>
* <code>get</code>
* <code>remove</code>
* <code>expire</code>
* </pre>
*
* @param <T> the type of value objects of the cache.
* @author Tigran Mkrtchyan
*/
public interface CacheEventListener<K,V> {
/**
* Fired after the entry is added into the cache.
* @param cache {@link Cache} into which the entry was put.
* @param v
*/
void notifyPut(Cache<K,V> cache, V v);
/**
* Fired after the valid (existing, not expired) is found.
* @param cache {@link Cache} in which the value is stored.
* @param v
*/
void notifyGet(Cache<K,V> cache, V v);
/**
* Fired after a valid (existing, not expired) entry was removed from
* the {@link Cache} <code>storage</code>
* @param cache {@link Cache} from which the value was removed.
* @param v
*/
void notifyRemove(Cache<K,V> cache, V v);
/**
* Fired when an entry was found to have expired.
* @param cache {@link Cache} from which the value was expired.
* @param v entry
*/
void notifyExpired(Cache<K,V> cache, V v);
}
/*
* This library is free software; you can redistribute it and/or modify
* it under the terms of the GNU Library General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This library 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 Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this program (see the file COPYING.LIB for more
* details); if not, write to the Free Software Foundation, Inc.,
* 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package org.dcache.utils;
/**
* NOP implementation of {@link CacheEventListener}.
*
* @param <T>
*/
public class NopCacheEventListener<K, V> implements CacheEventListener<K,V> {
public void notifyPut(Cache<K,V> cache, V v) {}
public void notifyGet(Cache<K,V> cache, V v) {}
public void notifyRemove(Cache<K,V> cache, V v) {}
public void notifyExpired(Cache<K,V> cache, V v) {}
}
/*
* This library is free software; you can redistribute it and/or modify
* it under the terms of the GNU Library General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This library 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 Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this program (see the file COPYING.LIB for more
* details); if not, write to the Free Software Foundation, Inc.,
* 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package org.dcache.utils;
import java.util.concurrent.TimeUnit;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
public class CacheTest {
private Cache<String, String> _cache;
@Before
public void setUp() {
_cache = new Cache<String, String>("test cache", 10, TimeUnit.SECONDS.toMillis(5),
TimeUnit.SECONDS.toMillis(5));
}
@Test
public void testPutGet() {
_cache.put("key1", "value1");
String value = _cache.get("key1");
assertEquals("received object not equal", "value1", value);
}
@Test
public void testGetAfterTimeout() throws Exception {
_cache.put("key1", "value1");
TimeUnit.SECONDS.sleep(6);
String value = _cache.get("key1");
assertNull("Object not expired", value);
}
@Test
public void testGetAfterRemove() throws Exception {
_cache.put("key1", "value1");
_cache.remove("key1");
String value = _cache.get("key1");
assertNull("Object not removed", value);
}
@Test
public void testExpiredByTime() throws Exception {
_cache.put("key1", "value1");
TimeUnit.MILLISECONDS.sleep(_cache.getEntryIdleTime() + 1000);
String value = _cache.get("key1");
assertNull("Object not expired", value);
}