blob: 9afd5d196b038f3970c2cf1b1bb02be6c7f9322f [file] [log] [blame]
/*
* Copyright 2005 The Apache Software Foundation.
*
* 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.apache.jcs.yajcache.soft;
import java.io.Serializable;
import java.lang.ref.ReferenceQueue;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import org.apache.commons.lang.SerializationUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.jcs.yajcache.beans.CacheChangeSupport;
import org.apache.jcs.yajcache.beans.ICacheChangeListener;
import org.apache.jcs.yajcache.lang.annotation.*;
import org.apache.jcs.yajcache.config.PerCacheConfig;
import org.apache.jcs.yajcache.beans.ICacheChangeListener;
import org.apache.jcs.yajcache.beans.CacheChangeSupport;
import org.apache.jcs.yajcache.core.CacheEntry;
import org.apache.jcs.yajcache.core.CacheManager;
import org.apache.jcs.yajcache.core.CacheType;
import org.apache.jcs.yajcache.core.ICache;
import org.apache.jcs.yajcache.file.CacheFileContent;
import org.apache.jcs.yajcache.file.CacheFileContentType;
import org.apache.jcs.yajcache.file.CacheFileDAO;
import org.apache.jcs.yajcache.file.CacheFileUtils;
import org.apache.jcs.yajcache.lang.ref.KeyedRefCollector;
import org.apache.jcs.yajcache.lang.ref.KeyedSoftReference;
import org.apache.jcs.yajcache.util.CollectionUtils;
import org.apache.jcs.yajcache.util.EqualsUtils;
import org.apache.jcs.yajcache.util.concurrent.locks.IKeyedReadWriteLock;
import org.apache.jcs.yajcache.util.concurrent.locks.KeyedReadWriteLock;
/**
* Cache implemented using Soft References.
*
* @author Hanson Char
*/
@CopyRightApache
@TODO("Annotate the thread-safetyness of the methods")
public class SoftRefFileCache<V> implements ICache<V>
{
private static final boolean debug = true;
private Log log = debug ? LogFactory.getLog(this.getClass()) : null;
private final @NonNullable ReferenceQueue<V> refq = new ReferenceQueue<V>();
private final @NonNullable String name;
private final @NonNullable Class<V> valueType;
private final @NonNullable ConcurrentMap<String,KeyedSoftReference<String,V>> map;
private PerCacheConfig config;
private final @NonNullable KeyedRefCollector<String> collector;
private final IKeyedReadWriteLock<String> keyedRWLock = new KeyedReadWriteLock<String>();
private final @NonNullable CacheChangeSupport<V> cacheChangeSupport =
new CacheChangeSupport<V>(this);
private AtomicInteger countGet = new AtomicInteger(0);
private AtomicInteger countGetHitMemory = new AtomicInteger(0);
private AtomicInteger countGetHitFile = new AtomicInteger(0);
private AtomicInteger countGetMissMemory = new AtomicInteger(0);
private AtomicInteger countGetMiss = new AtomicInteger(0);
private AtomicInteger countGetCorruptedFile = new AtomicInteger(0);
private AtomicInteger countGetEmptyRef = new AtomicInteger(0);
private AtomicInteger countPut = new AtomicInteger(0);
private AtomicInteger countPutClearRef = new AtomicInteger(0);
private AtomicInteger countPutMissMemory = new AtomicInteger(0);
private AtomicInteger countPutNewMemoryValue = new AtomicInteger(0);
private AtomicInteger countPutNewFileValue = new AtomicInteger(0);
private AtomicInteger countPutSerializable = new AtomicInteger(0);
private AtomicInteger countPutReadFile = new AtomicInteger(0);
private AtomicInteger countPutWriteFile = new AtomicInteger(0);
private AtomicInteger countRemove = new AtomicInteger(0);
public @NonNullable String getName() {
return this.name;
}
public @NonNullable Class<V> getValueType() {
return this.valueType;
}
public SoftRefFileCache(
@NonNullable String name, @NonNullable Class<V> valueType,
int initialCapacity,float loadFactor, int concurrencyLevel)
{
this.map = CollectionUtils.inst.newConcurrentHashMap(initialCapacity, loadFactor, concurrencyLevel);
this.collector = new KeyedRefCollector<String>(refq, map);
this.name = name;
this.valueType = valueType;
CacheFileUtils.inst.mkCacheDirs(this.name);
}
public SoftRefFileCache(
@NonNullable String name, @NonNullable Class<V> valueType,
int initialCapacity)
{
this.map = CollectionUtils.inst.newConcurrentHashMap(initialCapacity);
this.collector = new KeyedRefCollector<String>(refq, map);
this.name = name;
this.valueType = valueType;
CacheFileUtils.inst.mkCacheDirs(this.name);
}
public SoftRefFileCache(@NonNullable String name,
@NonNullable Class<V> valueType)
{
this.map = CollectionUtils.inst.newConcurrentHashMap();
this.collector = new KeyedRefCollector<String>(refq, map);
this.name = name;
this.valueType = valueType;
CacheFileUtils.inst.mkCacheDirs(this.name);
}
/** Only an approximation. */
public boolean isEmpty() {
return this.isMemoryCacheEmpty() && this.isCacheDirEmpty();
}
public boolean isMemoryCacheEmpty() {
this.collector.run();
return this.map.isEmpty();
}
public boolean isCacheDirEmpty() {
Lock cacheLock = CacheManager.inst.readLock(this);
cacheLock.lock();
try {
return CacheFileUtils.inst.isCacheDirEmpty(this.name);
} finally {
cacheLock.unlock();
}
}
public int size() {
return Math.max(this.getMemoryCacheSize(), this.getCacheDirSize());
}
public int getMemoryCacheSize() {
this.collector.run();
return this.map.size();
}
public int getCacheDirSize() {
Lock cacheLock = CacheManager.inst.readLock(this);
cacheLock.lock();
try {
return CacheFileUtils.inst.getCacheDirSize(this.name);
} finally {
cacheLock.unlock();
}
}
// @tothink: SoftReference.get() doesn't seem to be thread-safe.
// But do we really want to synchronize upon invoking get() ?
// It's not thread-safe, but what's the worst consequence ?
public V get(@NonNullable String key) {
collector.run();
if (debug)
this.countGet.incrementAndGet();
Lock cacheLock = CacheManager.inst.readLock(this);
cacheLock.lock();
try {
Lock lock = this.keyedRWLock.readLock(key);
lock.lock();
try {
return doGet(key);
} finally {
lock.unlock();
}
} finally {
cacheLock.unlock();
}
}
private V doGet(String key) {
KeyedSoftReference<String,V> ref = map.get(key);
V val = null;
if (ref != null) {
val = ref.get();
if (debug) {
if (val == null)
this.countGetEmptyRef.incrementAndGet();
}
}
else {
if (debug)
this.countGetMissMemory.incrementAndGet();
}
if (val == null) {
// Not in memory.
if (ref != null) {
// Rarely gets here, if ever.
// GC'd. So try to clean up the key/ref pair.
this.map.remove(key, ref);
}
CacheFileContent cfc = CacheFileDAO.inst.readCacheItem(this.name, key);
if (cfc == null) {
// Not in file system.
if (debug)
this.countGetMiss.incrementAndGet();
return null;
}
// Found in file system.
if (debug)
this.countGetHitFile.incrementAndGet();
val = (V)cfc.deserialize();
if (val == null) {
// Corrupted file. Try remove it from file system.
if (debug)
this.countGetCorruptedFile.incrementAndGet();
// Don't think I need to put a read lock on the cache for removal.
CacheFileDAO.inst.removeCacheItem(this.name, key);
return null;
}
// Resurrect item back to memory.
map.putIfAbsent(key,
new KeyedSoftReference<String,V>(key, val, refq));
}
else {
if (debug)
this.countGetHitMemory.incrementAndGet();
}
// cache value exists.
return val;
}
// private void renewSoftReference(String key, V val) {
// if (debug)
// log.debug("get: try to refresh the soft reference.");
// KeyedSoftRef<V> oldRef =
// map.put(key, new KeyedSoftRef<V>(key, val, refq));
// // Check for race conditon.
// if (oldRef == null) {
// // key has just been removed by another thread.
// if (debug)
// log.debug("get: key has just been removed by another thread.");
// return;
// }
// V oldVal = oldRef.get();
// // if oldVal is null, it means the GC just cleared it.
// while (oldVal != null && oldVal != val) {
// // race condition occurred
// // put back the old stuff
// if (debug)
// log.debug("get: race condition occurred. put back the old stuff");
// val = oldVal;
// oldRef = map.put(key, oldRef);
//
// if (oldRef == null) {
// // key has just been removed by another thread.
// if (debug)
// log.debug("get: key has just been removed by another thread.");
// oldRef = map.remove(key);
//
// if (oldRef == null) {
// // again, key has just been removed by another thread.
// if (debug)
// log.debug("again: key has just been removed by another thread.");
// break;
// }
// }
// oldVal = oldRef.get();
// }
// return;
// }
public V get(@NonNullable Object key) {
return this.get(key.toString());
}
public V put(@NonNullable String key, @NonNullable V value) {
this.collector.run();
if (debug)
this.countPut.incrementAndGet();
Lock cacheLock = CacheManager.inst.readLock(this);
cacheLock.lock();
try {
Lock lock = this.keyedRWLock.writeLock(key);
lock.lock();
try {
return doPut(key, value);
} finally {
lock.unlock();
}
} finally {
cacheLock.unlock();
}
}
private V doPut(@NonNullable String key, @NonNullable V value) {
KeyedSoftReference<String,V> oldRef =
map.put(key, new KeyedSoftReference<String,V>(key, value, refq));
V ret = null;
if (oldRef != null) {
ret = oldRef.get();
oldRef.clear();
if (debug)
this.countPutClearRef.incrementAndGet();
}
if (ret == null) {
// Not in memory.
if (debug)
this.countPutMissMemory.incrementAndGet();
if (value instanceof Serializable) {
// Try the file system.
if (debug)
this.countPutSerializable.incrementAndGet();
CacheFileContent cfc = CacheFileDAO.inst.readCacheItem(this.name, key);
if (cfc != null) {
if (debug)
this.countPutReadFile.incrementAndGet();
ret = (V)cfc.deserialize();
}
if (!EqualsUtils.inst.equals(value, ret)) {
// Considered new value being put to memory.
// So persist to file system.
if (debug) {
this.countPutNewFileValue.incrementAndGet();
this.countPutWriteFile.incrementAndGet();
}
byte[] ba = SerializationUtils.serialize((Serializable)value);
CacheFileDAO.inst.writeCacheItem(
this.name, CacheFileContentType.JAVA_SERIALIZATION, key, ba);
}
}
return ret;
}
// ret must be non-null.
// Found in memory
if (!EqualsUtils.inst.equals(value, ret)) {
if (debug)
this.countPutNewMemoryValue.incrementAndGet();
// Different value being put to memory.
if (value instanceof Serializable) {
// Persist to file system.
if (debug) {
this.countPutSerializable.incrementAndGet();
this.countPutWriteFile.incrementAndGet();
}
byte[] ba = SerializationUtils.serialize((Serializable)value);
CacheFileDAO.inst.writeCacheItem(
this.name, CacheFileContentType.JAVA_SERIALIZATION, key, ba);
}
}
return ret;
}
@TODO(
value="Queue up a flush beans for the key.",
details="This is useful for synchronizing caches in a cluster environment."
)
private void publishFlushKey(@NonNullable String key) {
}
public void putAll(@NonNullable Map<? extends String, ? extends V> map) {
for (final Map.Entry<? extends String, ? extends V> e : map.entrySet())
this.put(e.getKey(), e.getValue());
}
public V remove(@NonNullable String key) {
this.collector.run();
if (debug)
this.countRemove.incrementAndGet();
Lock cacheLock = CacheManager.inst.readLock(this);
cacheLock.lock();
try {
Lock lock = this.keyedRWLock.writeLock(key);
lock.lock();
try {
return doRemove(key);
} finally {
lock.unlock();
}
} finally {
cacheLock.unlock();
}
}
private V doRemove(@NonNullable String key) {
KeyedSoftReference<String,V> oldRef = map.remove(key);
V ret = null;
if (oldRef != null) {
// may exist in memory
ret = oldRef.get();
oldRef.clear();
}
if (ret == null) {
// not exist or no longer exist in memory;
// so check the file system.
CacheFileContent cfc = CacheFileDAO.inst.readCacheItem(this.name, key);
if (cfc == null) {
// not exist in file system.
return null;
}
if (cfc != null) {
// If corrupted, invoking deserialize will return null.
ret = (V)cfc.deserialize();
}
}
// Must exist the file system, corrupted or not.
// Don't think I need to put a read lock on the cache for removal.
CacheFileDAO.inst.removeCacheItem(this.name, key);
return ret;
}
public V remove(@NonNullable Object key) {
return key == null ? null : this.remove(key.toString());
}
public void clear() {
for (String key : this.map.keySet())
this.remove(key);
}
public @NonNullable Set<String> keySet() {
Set<String> kset = map.keySet();
String[] list = null;
Lock cacheLock = CacheManager.inst.readLock(this);
cacheLock.lock();
try {
list = CacheFileUtils.inst.getCacheDirList(this.name);
} finally {
cacheLock.unlock();
}
if (list != null)
kset.addAll(Arrays.asList(list));
return kset;
}
@UnsupportedOperation
public Set<Map.Entry<String,V>> entrySet() {
throw new UnsupportedOperationException("Only memoryEntrySet and keySet are supported.");
}
public @NonNullable Set<Map.Entry<String,V>> memoryEntrySet() {
// this.collector.run();
Set<Map.Entry<String,KeyedSoftReference<String,V>>> fromSet = map.entrySet();
Set<Map.Entry<String,V>> toSet = new HashSet<Map.Entry<String,V>>();
for (final Map.Entry<String, KeyedSoftReference<String,V>> item : fromSet) {
KeyedSoftReference<String,V> ref = item.getValue();
V val = ref.get();
if (val != null) {
Map.Entry<String,V> e = new CacheEntry<V>(item.getKey(), val);
toSet.add(e);
}
}
return toSet;
}
@UnsupportedOperation
public @NonNullable Collection<V> values() {
throw new UnsupportedOperationException("Only memoryValues and keySet are supported.");
}
public @NonNullable Collection<V> memoryValues() {
Collection<KeyedSoftReference<String,V>> fromSet = map.values();
List<V> toCol = new ArrayList<V>(fromSet.size());
for (final KeyedSoftReference<String,V> ref : fromSet) {
V val = ref.get();
if (val != null) {
toCol.add(val);
}
}
return toCol;
}
public boolean containsKey(@NonNullable Object key) {
return this.get(key.toString()) != null;
}
@UnsupportedOperation
public boolean containsValue(@NonNullable Object value) {
throw new UnsupportedOperationException("Only memoryContainsValue is supported.");
}
public boolean memoryContainsValue(@NonNullable Object value) {
Collection<KeyedSoftReference<String,V>> fromSet = map.values();
for (final KeyedSoftReference<String,V> ref : fromSet) {
V val = ref.get();
if (EqualsUtils.inst.equals(value, val))
return true;
}
return false;
}
/** Returns the number of Soft References collected by GC. */
// public int getCollectorCount() {
// return this.collector.getCount();
// }
public void addCacheChangeListener(@NonNullable ICacheChangeListener<V> listener)
{
this.cacheChangeSupport.addCacheChangeListener(listener);
}
public void removeCacheChangeListener(@NonNullable ICacheChangeListener<V> listener)
{
this.cacheChangeSupport.removeCacheChangeListener(listener);
}
public PerCacheConfig getConfig() {
return config;
}
public void setConfig(PerCacheConfig config) {
this.config = config;
}
@Implements(ICache.class)
public CacheType getCacheType() {
return CacheType.SOFT_REFERENCE_FILE;
}
@Override public String toString() {
return new ToStringBuilder(this)
.append("\n").append("name", this.getName())
.append("\n").append("valueType", this.getValueType().getName())
.append("\n").append("countGet", this.countGet)
.append("\n").append("countGetHitMemory", this.countGetHitMemory)
.append("\n").append("countGetHitFile", this.countGetHitFile)
.append("\n").append("countGetMissMemory", this.countGetMissMemory)
.append("\n").append("countGetEmptyRef", this.countGetEmptyRef)
.append("\n").append("countGetMiss", this.countGetMiss)
.append("\n").append("countGetCorruptedFile", this.countGetCorruptedFile)
.append("\n").append("countPut", this.countPut)
.append("\n").append("countPutClearRef", this.countPutClearRef)
.append("\n").append("countPutMissMemory", this.countPutMissMemory)
.append("\n").append("countPutNewFileValue", this.countPutNewFileValue)
.append("\n").append("countPutNewMemoryValue", this.countPutNewMemoryValue)
.append("\n").append("countPutReadFile", this.countPutReadFile)
.append("\n").append("countPutSerializable", this.countPutSerializable)
.append("\n").append("countPutWriteFile", this.countPutWriteFile)
.append("\n").append("countRemove", this.countRemove)
.append("\n").append("collector", this.collector)
.append("\n").append("keyedRWLock", this.keyedRWLock)
.toString();
}
}