package com.ctriposs.bigcache;
import java.io.File;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReadWriteLock;
import com.ctriposs.bigcache.lock.StripedReadWriteLock;
import com.ctriposs.bigcache.storage.Pointer;
import com.ctriposs.bigcache.storage.StorageBlock;
import com.ctriposs.bigcache.storage.StorageManager;
import com.ctriposs.bigcache.utils.FileUtil;
/**
* The Class BigCache is a cache that uses persistent storage
* to store or retrieve data in byte array. To do so,
* BigCache uses pointers to point location(offset) of an item within a persistent storage block.
* BigCache clears the storage periodically to gain free space if
* storage are dirty(storage holes because of deletion). It also does eviction depending on
* access time to the objects.
*
* @param <K> the key type
*/
public class BigCache<K> implements ICache<K> {
/** The Constant DELTA. */
//private static final float DELTA = 0.00001f;
/** The default purge interval which is 5 minutes. */
public static final long DEFAULT_PURGE_INTERVAL = 5 * 60 * 1000;
/** The default merge interval which is 10 minutes. */
public static final long DEFAULT_MERGE_INTERVAL = 10 * 60 * 1000;
/** The default threshold for dirty block recycling */
public static final double DEFAULT_DIRTY_RATIO_THRESHOLD = 0.5;
/** The Constant DEFAULT_CONCURRENCY_LEVEL. */
public static final int DEFAULT_CONCURRENCY_LEVEL = 8; // 256 concurrent level
/** The length of value can't be greater than 4m */
public static final int MAX_VALUE_LENGTH = 4 * 1024 * 1024;
/** The hit counter. */
protected AtomicLong hitCounter = new AtomicLong();
/** The miss counter. */
protected AtomicLong missCounter = new AtomicLong();
/** The get counter. */
protected AtomicLong getCounter = new AtomicLong();
/** The put counter. */
protected AtomicLong putCounter = new AtomicLong();
/** The delete counter. */
protected AtomicLong deleteCounter = new AtomicLong();
/** The # of purges due to expiration. */
protected AtomicLong purgeCounter = new AtomicLong();
/** The # of moves for dirty block recycle. */
protected AtomicLong moveCounter = new AtomicLong();
/** The total storage size we have used, including the expired ones which are still in the pointermap */
protected AtomicLong usedSize = new AtomicLong();
/** The internal map. */
protected final ConcurrentMap<K, CacheValueWrapper> pointerMap = new ConcurrentHashMap<K, CacheValueWrapper>();
/** Managing the storages. */
/* package for ut */ final StorageManager storageManager;
/** The read write lock. */
private final StripedReadWriteLock readWriteLock;
/** The times of merge procedure has run. */
private final AtomicLong NO_OF_MERGE_RUN = new AtomicLong();
/** the times of purge procedure has run. */
private final AtomicLong NO_OF_PURGE_RUN = new AtomicLong();
/** The directory to store cached data */
private String cacheDir;
/** The thread pool which is used to clean the cache */
private ScheduledExecutorService ses;
/** dirty ratio which controls block recycle */
private final double dirtyRatioThreshold;
public BigCache(String dir, CacheConfig config) throws IOException {
this.cacheDir = dir;
if (!this.cacheDir.endsWith(File.separator)) {
this.cacheDir += File.separator;
}
// validate directory
if (!FileUtil.isFilenameValid(this.cacheDir)) {
throw new IllegalArgumentException("Invalid cache data directory : " + this.cacheDir);
}
// clean up old cache data if exists
FileUtil.deleteDirectory(new File(this.cacheDir));
this.storageManager = new StorageManager(this.cacheDir, config.getCapacityPerBlock(),
config.getInitialNumberOfBlocks(), config.getStorageMode(), config.getMaxOffHeapMemorySize());
this.readWriteLock = new StripedReadWriteLock(config.getConcurrencyLevel());
ses = new ScheduledThreadPoolExecutor(2);
ses.scheduleWithFixedDelay(new CacheCleaner(this), config.getPurgeInterval(), config.getPurgeInterval(), TimeUnit.MILLISECONDS);
ses.scheduleWithFixedDelay(new CacheMerger(this), config.getMergeInterval(), config.getMergeInterval(), TimeUnit.MILLISECONDS);
dirtyRatioThreshold = config.getDirtyRatioThreshold();
}
@Override
public void put(K key, byte[] value) throws IOException {
this.put(key, value, -1); // -1 means no time to idle(never expires)
}
@Override
public void put(K key, byte[] value, long tti) throws IOException {
putCounter.incrementAndGet();
if (value == null || value.length > MAX_VALUE_LENGTH) {
throw new IllegalArgumentException("value is null or too long");
}
writeLock(key);
try {
CacheValueWrapper wrapper = pointerMap.get(key);
Pointer newPointer; // pointer with new storage info
if (wrapper == null) {
// create a new one
wrapper = new CacheValueWrapper();
newPointer = storageManager.store(value);
} else {
// update and get the new storage
Pointer oldPointer = wrapper.getPointer();
newPointer = storageManager.update(oldPointer, value);
usedSize.addAndGet(oldPointer.getLength() * -1);
}
wrapper.setPointer(newPointer);
wrapper.setTimeToIdle(tti);
wrapper.setLastAccessTime(System.currentTimeMillis());
usedSize.addAndGet(newPointer.getLength());
pointerMap.put(key, wrapper);
} finally {
writeUnlock(key);
}
}
@Override
public byte[] get(K key) throws IOException {
getCounter.incrementAndGet();
readLock(key);
try {
CacheValueWrapper wrapper = pointerMap.get(key);
if (wrapper == null) {
missCounter.incrementAndGet();
return null;
}
synchronized (wrapper) { // this object may be modified in move thread, use lock here
if (!wrapper.isExpired()) {
// access time updated, the following change will not be lost
hitCounter.incrementAndGet();
wrapper.setLastAccessTime(System.currentTimeMillis());
return storageManager.retrieve(wrapper.getPointer());
} else {
missCounter.incrementAndGet();
return null;
}
}
} finally {
readUnlock(key);
}
}
@Override
public byte[] delete(K key) throws IOException {
deleteCounter.incrementAndGet();
writeLock(key);
try {
CacheValueWrapper wrapper = pointerMap.get(key);
if (wrapper != null) {
byte[] payload = storageManager.remove(wrapper.getPointer());
pointerMap.remove(key);
usedSize.addAndGet(payload.length * -1);
return payload;
}
} finally {
writeUnlock(key);
}
return null;
}
@Override
public boolean contains(K key) {
return pointerMap.containsKey(key);
}
/**
* Clear the cache and the underlying storage.
*
* Don't do any operation else before clean has finished.
*/
@Override
public void clear() {
this.storageManager.free();
/**
* we free storage first, so we can guarantee:
* 1. entries created/updated before "pointMap" clear process will not be seen. That's what free means.
* 2. entries created/updated after "pointMap" clear will be safe, as no free operation happens later
*
* There is only a small window of inconsistent state between the two free operation, and users should
* not see this if they behave right.
*/
this.pointerMap.clear();
this.usedSize.set(0);
}
@Override
public double hitRatio() {
return 1.0 * hitCounter.get() / (hitCounter.get() + missCounter.get());
}
/**
* Read Lock for key is locked.
*
* @param key the key
*/
protected void readLock(K key) {
readWriteLock.readLock(Math.abs(key.hashCode()));
}
/**
* Read Lock for key is unlocked.
*
* @param key the key
*/
protected void readUnlock(K key) {
readWriteLock.readUnlock(Math.abs(key.hashCode()));
}
/**
* Write Lock for key is locked..
*
* @param key the key
*/
protected void writeLock(K key) {
readWriteLock.writeLock(Math.abs(key.hashCode()));
}
/**
* Write Lock for key is unlocked.
*
* @param key the key
*/
protected void writeUnlock(K key) {
readWriteLock.writeUnlock(Math.abs(key.hashCode()));
}
/**
* Get the internal lock.
* @param key
* @return
*/
protected ReadWriteLock getLock(K key) {
return readWriteLock.getLock(Math.abs(key.hashCode()));
}
@Override
public void close() throws IOException {
this.clear();
this.ses.shutdownNow();
this.storageManager.close();
}
public long count(){
return pointerMap.size();
}
/**
* Get the latest stats of the cache.
*
* @return all stats.
*/
public BigCacheStats getStats() {
return new BigCacheStats(hitCounter.get(), missCounter.get(), getCounter.get(),
putCounter.get(), deleteCounter.get(), purgeCounter.get(), moveCounter.get(),
count(), storageManager.getUsed(), storageManager.getDirty(),
storageManager.getCapacity(), storageManager.getUsedBlockCount(), storageManager.getFreeBlockCount(),
storageManager.getTotalBlockCount());
}
abstract static class CacheDaemonWorker<K> implements Runnable {
private WeakReference<BigCache<K>> cacheHolder;
private ScheduledExecutorService ses;
CacheDaemonWorker(BigCache<K> cache) {
ses = cache.ses;
cacheHolder = new WeakReference<BigCache<K>>(cache);
}
@Override
public void run() {
BigCache cache = cacheHolder.get();
if (cache == null) {
// cache is recycled abnormally
if (ses != null) {
ses.shutdownNow();
ses = null;
}
return;
}
try {
process(cache);
} catch (IOException e) {
e.printStackTrace();
}
cache.storageManager.clean();
}
abstract void process(BigCache<K> cache) throws IOException;
}
/**
* Clean the expired keys.
*
* @param <K>
*/
static class CacheCleaner<K> extends CacheDaemonWorker<K> {
CacheCleaner(BigCache<K> cache) {
super(cache);
}
@Override
public void process(BigCache<K> cache) throws IOException {
Set<K> keys = cache.pointerMap.keySet();
// store the expired keys according to their associated lock
Map<ReadWriteLock, List<K>> expiredKeys = new HashMap<ReadWriteLock, List<K>>();
// find all the keys that may be expired. It's lock less as we will validate later.
for(K key : keys) {
CacheValueWrapper wrapper = cache.pointerMap.get(key);
if (wrapper != null && wrapper.isExpired()) {
ReadWriteLock lock = cache.getLock(key);
List<K> keyList = expiredKeys.get(lock);
if (keyList == null) {
keyList = new ArrayList<K>();
expiredKeys.put(lock, keyList);
}
keyList.add(key);
}
}
// expire keys with write lock, this will complete quickly.
for (ReadWriteLock lock : expiredKeys.keySet()) {
List<K> keyList = expiredKeys.get(lock);
if (keyList == null || keyList.isEmpty()) {
continue;
}
lock.writeLock().lock();
try {
for(K key : keyList) {
CacheValueWrapper wrapper = cache.pointerMap.get(key);
if (wrapper != null && wrapper.isExpired()) { // double check
Pointer oldPointer = wrapper.getPointer();
cache.usedSize.addAndGet(oldPointer.getLength() * -1);
cache.storageManager.removeLight(oldPointer);
cache.pointerMap.remove(key);
cache.purgeCounter.incrementAndGet();
}
}
} finally {
lock.writeLock().unlock();
}
}
cache.NO_OF_PURGE_RUN.incrementAndGet();
}
}
static class CacheMerger<K> extends CacheDaemonWorker<K> {
CacheMerger(BigCache<K> cache) {
super(cache);
}
@Override
void process(BigCache<K> cache) throws IOException {
Set<K> keys = cache.pointerMap.keySet();
// store the keys in dirty block according to the block index
Map<Integer, List<K>> keysInDirtyBlock = new HashMap<Integer, List<K>>();
// find all the keys that need to be moved. It's lock less as we will validate later.
for(K key : keys) {
CacheValueWrapper wrapper = cache.pointerMap.get(key);
StorageBlock sb;
Pointer pointer;
if (wrapper != null
&& ((pointer = wrapper.getPointer()) != null)
&& ((sb = pointer.getStorageBlock()) != null)
&& (sb.getDirtyRatio() > cache.dirtyRatioThreshold)) {
Integer index = sb.getIndex();
List<K> keyList = keysInDirtyBlock.get(index);
if (keyList == null) {
keyList = new ArrayList<K>();
keysInDirtyBlock.put(index, keyList);
}
keyList.add(key);
}
}
// move keys index by index, we will always work on the block in memory.
for(List<K> keyList : keysInDirtyBlock.values()) {
if (keyList == null || keyList.isEmpty()) {
continue;
}
for(K key : keyList) {
cache.readLock(key);
try {
CacheValueWrapper wrapper = cache.pointerMap.get(key);
if (wrapper == null) {
// not exist now and do nothing, continue with next key;
continue;
}
// wrapper is accessed/modified by reader and the merger, use lock here
synchronized (wrapper) {
StorageBlock sb = wrapper.getPointer().getStorageBlock();
if (sb.getDirtyRatio() > cache.dirtyRatioThreshold) {
byte[] payload = cache.storageManager.remove(wrapper.getPointer());
Pointer newPointer = cache.storageManager.storeExcluding(payload, sb);
wrapper.setPointer(newPointer);
cache.moveCounter.incrementAndGet();
}
}
} finally {
cache.readUnlock(key);
}
}
}
cache.NO_OF_MERGE_RUN.incrementAndGet();
}
}
}