/*
* Copyright (c) 1999 Dustin Sallings
*/
package net.spy.cache;
import java.io.IOException;
import java.lang.ref.Reference;
import java.net.InetAddress;
import java.util.Iterator;
import java.util.Map;
import net.spy.SpyObject;
import net.spy.SpyThread;
import net.spy.util.TimeStampedHashMap;
/**
* Spy in-memory cache object.
*
* <p>
*
* If the system properties <tt>net.spy.cache.multi.addr</tt> and
* <tt>net.spy.cache.multi.port</tt> are both set, requests may be sent as
* ASCII strings on that multicast group and port to clear cache entries
* based on prefix.
*/
public class SpyCache extends SpyObject {
final TimeStampedHashMap<String, Cachable> cacheStore;
private SpyCacheCleaner cacheCleaner=null;
CacheDelegate delegate=null;
private static SpyCache instance=null;
// how frequently to clean up the cache
private static final int CACHE_CLEAN_SLEEP_TIME=60000;
/**
* Construct a new instance of SpyCache. This allows subclasses to
* override certain methods, allowing smarter cache handling.
*/
protected SpyCache() {
super();
cacheStore=new TimeStampedHashMap<String, Cachable>();
delegate=new DummyDelegate();
}
private synchronized void checkThread() {
if(cacheCleaner==null || (!cacheCleaner.isAlive())) {
cacheCleaner=new SpyCacheCleaner();
}
}
/**
* Get the instance of SpyCache.
*
* @return the instance of SpyCache, or a new instance if required
*/
public static synchronized SpyCache getInstance() {
if(instance==null) {
instance=new SpyCache();
}
instance.checkThread();
return(instance);
}
/**
* Shut down the cache.
*/
public static synchronized void shutdown() {
if(instance != null && instance.cacheCleaner != null) {
instance.cacheCleaner.shutdown();
}
instance=null;
}
/**
* Set the delegate for this SpyCache.
*
* @param del
*/
public void setDelegate(CacheDelegate del) {
if(del == null) {
throw new NullPointerException("Invalid delegate <null>");
}
delegate=del;
}
/**
* Store a Cachable object in the cache.
*
* @param key the key for storing this object
* @param value the object to store
*/
public void store(String key, Cachable value) {
synchronized(cacheStore) {
// Send the cached event notify to the cachable itself
value.cachedEvent(key);
cacheStore.put(key, value);
}
delegate.cachedObject(key, value);
}
/**
* Store an object in the cache with the specified timeout.
*
* <p>
* Objects will be wrapped in a private subclass of {@link Cachable}
* that expires based on the time.
* </p>
*
* @param key Cache key
* @param value Object to cache
* @param cacheTime Amount of time (in milliseconds) to store object.
*/
public void store(String key, Object value, long cacheTime) {
Cachable i=new SpyCacheItem(key, value, cacheTime);
store(key, i);
}
/**
* Get an object from the cache, returns null if there's not a valid
* object in the cache with this key.
*
* @param key key of the object to return
* @return the object, else null
*/
public Object get(String key) {
Object ret=null;
long t=System.currentTimeMillis();
synchronized(cacheStore) {
Cachable i=cacheStore.get(key);
if(i!=null && (!i.isExpired())) {
// mark the object as seen
i.setAccessTime(t);
// get the object from the cache
ret=i.getCachedObject();
// If the stored object is a reference, dereference it.
if((ret!=null) && (ret instanceof Reference)) {
Reference<?> ref=(Reference<?>)ret;
ret=ref.get();
} // Object was a reference
} // Found object in cache
} // Locked the cache store
return(ret);
}
/**
* Manually remove an object from the cache.
*
* @param key key to remove
*/
public void uncache(String key) {
Cachable unc=null;
synchronized(cacheStore) {
unc=cacheStore.remove(key);
}
if(unc!=null) {
unc.uncachedEvent(key);
delegate.uncachedObject(key, unc);
}
}
/**
* Remove all objects from the cache that begin with the passed in
* string.
*
* @param keystart string to match in the key name
*/
public void uncacheLike(String keystart) {
synchronized(cacheStore) {
for(Iterator<Map.Entry<String, Cachable>> i
=cacheStore.entrySet().iterator(); i.hasNext();) {
Map.Entry<String, Cachable> me=i.next();
String key=me.getKey();
// If this matches, kill it.
if(key.startsWith(keystart)) {
i.remove();
Cachable c=me.getValue();
c.uncachedEvent(key);
delegate.uncachedObject(key, c);
}
} // for loop
} // lock
}
////////////////////////////////////////////////////////////////////
// Private Classes //
////////////////////////////////////////////////////////////////////
private class SpyCacheCleaner extends SpyThread {
// How many cleaning passes we've done.
private int passes=0;
// This is so we can only report multicast security exceptions
// once.
private boolean reportedMulticastSE=false;
// Insert multicast listener here.
private CacheClearRequestListener listener=null;
// This indicates whether we need to go through the multicast cache
// clearer loop. It's true by default so we'll try it the first time.
private boolean wantMulticastListener=true;
private boolean shutdown=false;
public SpyCacheCleaner() {
super();
setName("SpyCacheCleaner");
setDaemon(true);
start();
}
public void shutdown() {
getLogger().debug("Shutting down %s", this);
shutdown=true;
synchronized(this) {
notifyAll();
}
}
@Override
public String toString() {
return(super.toString() + " - "
+ passes + " runs, mod age: " + cacheStore.getUseAge()
+ ", cur size: " + cacheStore.size()
+ ", tot stored: " + cacheStore.getNumPuts()
+ ", watermark: " + cacheStore.getWatermark()
+ ", hits: " + cacheStore.getHits()
+ ", misses: " + cacheStore.getMisses()
);
}
private void cleanup() throws Exception {
synchronized(cacheStore) {
for(Iterator<Map.Entry<String, Cachable>> i
=cacheStore.entrySet().iterator(); i.hasNext();){
Map.Entry<String, Cachable> me=i.next();
String key=me.getKey();
Cachable it=me.getValue();
if(it.isExpired()) {
getLogger().debug("%s expired", it.getCacheKey());
i.remove();
it.uncachedEvent(key);
delegate.uncachedObject(key, it);
}
}
}
passes++;
}
private boolean shouldIContinue() {
boolean rv=false;
// Return true if the difference between now and the last
// time the cache was touched is less than an hour.
if((!shutdown) && (cacheStore.getUseAge()) < (3600*1000)) {
rv=true;
}
return(rv);
}
// Make sure our multicast listener is still running if it should be.
private void checkMulticastThread() {
try {
String addrS=System.getProperty("net.spy.cache.multi.addr");
String portS=System.getProperty("net.spy.cache.multi.port");
if(addrS!=null && portS!=null) {
wantMulticastListener=true;
int port=Integer.parseInt(portS);
InetAddress group = InetAddress.getByName(addrS);
listener=new CacheClearRequestListener(group, port);
} else {
wantMulticastListener=false;
}
} catch(SecurityException se) {
// Only do this the first time.
if(!reportedMulticastSE) {
getLogger().error("Couldn't create multicast listener", se);
reportedMulticastSE=true;
}
} catch(IOException ioe) {
getLogger().error("Couldn't create multicast listener", ioe);
}
}
/**
* Loop until there's no need to loop any more.
*/
@Override
public void run() {
// It will keep going until nothing's been touched in the cache
// for an hour, at which point it'll dump the whole cache and join.
while(shouldIContinue()) {
try {
// Just for throttling, sleep a second.
synchronized(this) {
wait(CACHE_CLEAN_SLEEP_TIME);
}
cleanup();
// Check to see if we want a multicast listener
if(wantMulticastListener
&& (listener==null || (!listener.isAlive()))) {
checkMulticastThread();
}
} catch(Exception e) {
getLogger().warn("Exception in cleanup loop", e);
}
}
getLogger().info("Shutting down.");
// OK, we're about to bail, let's dump the cache and go.
synchronized(cacheStore) {
for(Iterator<Map.Entry<String, Cachable>> i
=cacheStore.entrySet().iterator(); i.hasNext();){
Map.Entry<String, Cachable> me=i.next();
String key=me.getKey();
Cachable it=me.getValue();
i.remove();
it.uncachedEvent(key);
delegate.uncachedObject(key, it);
}
}
// Tell the multicast listener to stop if we have one
if(listener!=null) {
listener.stopRunning();
}
}
} // Cleaner class
static class SpyCacheItem extends AbstractCachable {
private final long exptime;
public SpyCacheItem(Object key, Object value, long cacheTime) {
super(key, value);
exptime=getCacheTime()+cacheTime;
}
@Override
public String toString() {
String out="{Cached item: " + getCacheKey();
if(exptime>0) {
out+=" Expires: " + new java.util.Date(exptime)
+ " - expired? " + isExpired();
}
out+="}";
return(out);
}
public boolean isExpired() {
boolean ret=false;
if(exptime>0) {
long t=System.currentTimeMillis();
ret=(t>exptime);
}
// If the value is a reference that is no longer valid,
// the object has expired
Object v=getCachedObject();
if(v instanceof Reference) {
Reference<?> rvalue=(Reference<?>)v;
if(rvalue.get()==null) {
ret=false;
}
}
return(ret);
}
} // SpyCacheItem
// This allows us to always have a delegate object registered.
static class DummyDelegate extends Object implements CacheDelegate {
public void cachedObject(String key, Cachable value) {
// Dummy implementation
}
public void uncachedObject(String key, Cachable value) {
// dummy implementation
}
}
}