/** * Copyright 2008 ThimbleWare Inc. * * 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 com.thimbleware.jmemcached; import org.apache.mina.common.ByteBuffer; import static java.lang.Integer.parseInt; import static java.lang.String.valueOf; import java.util.Set; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.DelayQueue; import java.util.concurrent.Delayed; import java.util.concurrent.TimeUnit; /** */ public class Cache { private int currentItems; private int totalItems; private int getCmds; private int setCmds; private int getHits; private int getMisses; private long bytesRead; private long bytesWritten; private long casCounter; protected CacheStorage cacheStorage; private DelayQueue<DelayedMCElement> deleteQueue; private final ReadWriteLock deleteQueueReadWriteLock; public enum StoreResponse { STORED, NOT_STORED, EXISTS, NOT_FOUND } public enum DeleteResponse { DELETED, NOT_FOUND } /** * Read-write lock allows maximal concurrency, since readers can share access; * only writers need sole access. */ private final ReadWriteLock cacheReadWriteLock; /** * Delayed key blocks get processed occasionally. */ private class DelayedMCElement implements Delayed { private MCElement element; public DelayedMCElement(MCElement element) { this.element = element; } public long getDelay(TimeUnit timeUnit) { return timeUnit.convert(element.blocked_until - Now(), TimeUnit.MILLISECONDS); } public int compareTo(Delayed delayed) { if (!(delayed instanceof DelayedMCElement)) return -1; else return element.keystring.compareTo(((DelayedMCElement)delayed).element.keystring); } } /** * Construct the server session handler * * @param cacheStorage the cache to use */ public Cache(CacheStorage cacheStorage) { initStats(); this.cacheStorage = cacheStorage; this.deleteQueue = new DelayQueue<DelayedMCElement>(); cacheReadWriteLock = new ReentrantReadWriteLock(); deleteQueueReadWriteLock = new ReentrantReadWriteLock(); } /** * Handle the deletion of an item from the cache. * * @param key the key for the item * @param time an amount of time to block this entry in the cache for further writes * @return the message response */ public DeleteResponse delete(String key, int time) { try { startCacheWrite(); if (isThere(key)) { if (time != 0) { // mark it as blocked MCElement el = this.cacheStorage.get(key); el.blocked = true; el.blocked_until = Now() + time; // actually clear the data since we don't need to keep it el.data_length = 0; el.data = new byte[0]; this.cacheStorage.put(key, el, el.data_length); // this must go on a queue for processing later... try { deleteQueueReadWriteLock.writeLock().lock(); deleteQueue.add(new DelayedMCElement(el)); } finally { deleteQueueReadWriteLock.writeLock().unlock(); } } else { this.cacheStorage.remove(key); // just remove it } return DeleteResponse.DELETED; } else { return DeleteResponse.NOT_FOUND; } } finally { finishCacheWrite(); } } /** * Executed periodically to clean from the cache those entries that are just blocking * the insertion of new ones. */ public void processDeleteQueue() { try { deleteQueueReadWriteLock.writeLock().lock(); DelayedMCElement toDelete = deleteQueue.poll(); if (toDelete != null) { try { startCacheWrite(); if (this.cacheStorage.get(toDelete.element.keystring) != null) { this.cacheStorage.remove(toDelete.element.keystring); } } finally { finishCacheWrite(); } } } finally { deleteQueueReadWriteLock.writeLock().unlock(); } } /** * Add an element to the cache * * @param e the element to add * @return the store response code */ public StoreResponse add(MCElement e) { try { startCacheWrite(); if (!isThere(e.keystring)) return set(e); else return StoreResponse.NOT_STORED; } finally { finishCacheWrite(); } } /** * Replace an element in the cache * * @param e the element to replace * @return the store response code */ public StoreResponse replace(MCElement e) { try { startCacheWrite(); if (isThere(e.keystring)) return set(e); else return StoreResponse.NOT_STORED; } finally { finishCacheWrite(); } } /** * Append bytes to the end of an element in the cache * * @param element the element to append * @return the store response code */ public StoreResponse append(MCElement element) { try { startCacheWrite(); MCElement ret = get(element.keystring); if (ret == null || isBlocked(ret) || isExpired(ret)) return StoreResponse.NOT_FOUND; else { ret.data_length += element.data_length; ByteBuffer b = ByteBuffer.allocate(ret.data_length); b.put(ret.data); b.put(element.data); ret.data = new byte[ret.data_length]; b.flip(); b.get(ret.data); ret.cas_unique++; this.cacheStorage.put(ret.keystring, ret, ret.data_length); return StoreResponse.STORED; } } finally { finishCacheWrite(); } } /** * Prepend bytes to the end of an element in the cache * * @param element the element to append * @return the store response code */ public StoreResponse prepend(MCElement element) { try { startCacheWrite(); MCElement ret = get(element.keystring); if (ret == null || isBlocked(ret) || isExpired(ret)) return StoreResponse.NOT_FOUND; else { ret.data_length += element.data_length; ByteBuffer b = ByteBuffer.allocate(ret.data_length); b.put(element.data); b.put(ret.data); ret.data = new byte[ret.data_length]; b.flip(); b.get(ret.data); ret.cas_unique++; this.cacheStorage.put(ret.keystring, ret, ret.data_length); return StoreResponse.STORED; } } finally { finishCacheWrite(); } } /** * Set an element in the cache * * @param e the element to set * @return the store response code */ public StoreResponse set(MCElement e) { try { startCacheWrite(); setCmds += 1;//update stats // increment the CAS counter; put in the new CAS e.cas_unique = casCounter++; this.cacheStorage.put(e.keystring, e, e.data_length); return StoreResponse.STORED; } finally { finishCacheWrite(); } } /** * Set an element in the cache but only if the element has not been touched * since the last 'gets' * @param cas_key the cas key returned by the last gets * @param e the element to set * @return the store response code */ public StoreResponse cas(Long cas_key, MCElement e) { try { startCacheWrite(); // have to get the element MCElement element = get(e.keystring); if (element == null || isBlocked(element)) return StoreResponse.NOT_FOUND; if (element.cas_unique == cas_key) { // cas_unique matches, now set the element return set(e); } else { // cas didn't match; someone else beat us to it return StoreResponse.EXISTS; } } finally { finishCacheWrite(); } } /** * Increment an (integer) element inthe cache * @param key the key to increment * @param mod the amount to add to the value * @return the message response */ public Integer get_add(String key, int mod) { try { startCacheWrite(); MCElement e = this.cacheStorage.get(key); if (e == null) { getMisses += 1;//update stats return null; } if (isExpired(e) || e.blocked) { //logger.info("FOUND BUT EXPIRED"); getMisses += 1;//update stats return null; } // TODO handle parse failure! int old_val = parseInt(new String(e.data)) + mod; // change value if (old_val < 0) { old_val = 0; } // check for underflow e.data = valueOf(old_val).getBytes(); // toString e.data_length = e.data.length; // assign new cas id e.cas_unique = casCounter++; this.cacheStorage.put(e.keystring, e, e.data_length); // save new value return old_val; } finally { finishCacheWrite(); } } /** * Check whether an element is in the cache and non-expired and the slot is non-blocked * @param key the key for the element to lookup * @return whether the element is in the cache and is live */ protected boolean isThere(String key) { try { startCacheRead(); MCElement e = this.cacheStorage.get(key); return e != null && !isExpired(e) && !isBlocked(e); } finally { finishCacheRead(); } } protected boolean isBlocked(MCElement e) { return e.blocked && e.blocked_until > Now(); } protected boolean isExpired(MCElement e) { return e.expire != 0 && e.expire < Now(); } /** * Get an element from the cache * @param key the key for the element to lookup * @return the element, or 'null' in case of cache miss. */ public MCElement get(String key) { getCmds += 1;//updates stats try { startCacheRead(); MCElement e = this.cacheStorage.get(key); if (e == null) { getMisses += 1;//update stats return null; } if (isExpired(e) || e.blocked) { getMisses += 1;//update stats return null; } getHits += 1;//update stats return e; } finally { finishCacheRead(); } } /** * Flush all cache entries * @return command response */ public boolean flush_all() { return flush_all(0); } /** * Flush all cache entries with a timestamp after a given expiration time * @param expire the flush time in seconds * @return command response */ public boolean flush_all(int expire) { // TODO implement this, it isn't right... but how to handle efficiently? (don't want to linear scan entire cacheStorage) try { startCacheWrite(); this.cacheStorage.clear(); } finally { finishCacheWrite(); } return true; } /** * @return the current time in seconds (from epoch), used for expiries, etc. */ protected final int Now() { return (int) (System.currentTimeMillis() / 1000); } /** * Initialize all statistic counters */ protected void initStats() { currentItems = 0; totalItems = 0; getCmds = setCmds = getHits = getMisses = 0; } public Set<String> keys() { try { startCacheRead(); return cacheStorage.keys(); } finally { finishCacheRead(); } } public long getCurrentItems() { try { startCacheRead(); return this.cacheStorage.count(); } finally { finishCacheRead(); } } public long getLimitMaxBytes() { try { startCacheRead(); return this.cacheStorage.getMaximumSize(); } finally { finishCacheRead(); } } public long getCurrentBytes() { try { startCacheRead(); return this.cacheStorage.getSize(); } finally { finishCacheRead(); } } /** * Blocks of code in which the contents of the cache * are examined in any way must be surrounded by calls to <code>startRead</code> * and <code>finishRead</code>. See documentation for ReadWriteLock. */ private void startCacheRead() { cacheReadWriteLock.readLock().lock(); } /** * Blocks of code in which the contents of the cache * are examined in any way must be surrounded by calls to <code>startRead</code> * and <code>finishRead</code>. See documentation for ReadWriteLock. */ private void finishCacheRead() { cacheReadWriteLock.readLock().unlock(); } /** * Blocks of code in which the contents of the cache * are changed in any way must be surrounded by calls to <code>startWrite</code> and * <code>finishWrite</code>. See documentation for ReadWriteLock. * protect the higher layers from implementation details. */ private void startCacheWrite() { cacheReadWriteLock.writeLock().lock(); } /** * Blocks of code in which the contents of the cache * are changed in any way must be surrounded by calls to <code>startWrite</code> and * <code>finishWrite</code>. See documentation for ReadWriteLock. */ private void finishCacheWrite() { cacheReadWriteLock.writeLock().unlock(); } public int getTotalItems() { return totalItems; } public int getGetCmds() { return getCmds; } public int getSetCmds() { return setCmds; } public int getGetHits() { return getHits; } public int getGetMisses() { return getMisses; } public long getBytesRead() { return bytesRead; } public long getBytesWritten() { return bytesWritten; } }