/* * Copyright (c) 1998-2011 Caucho Technology -- all rights reserved * * This file is part of Resin(R) Open Source * * Each copy or derived work must preserve the copyright notice and this * notice unmodified. * * Resin Open Source is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * Resin Open Source 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, or any warranty * of NON-INFRINGEMENT. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with Resin Open Source; if not, write to the * * Free Software Foundation, Inc. * 59 Temple Place, Suite 330 * Boston, MA 02111-1307 USA * * @author Scott Ferguson */ package com.caucho.server.distcache; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.Locale; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Future; import javax.annotation.PostConstruct; import javax.cache.Cache; import javax.cache.CacheConfiguration; import javax.cache.CacheException; import javax.cache.CacheLoader; import javax.cache.CacheManager; import javax.cache.CacheStatistics; import javax.cache.Status; import javax.cache.event.CacheEntryListener; import javax.cache.event.NotificationScope; import com.caucho.config.ConfigException; import com.caucho.config.Configurable; import com.caucho.config.types.Period; import com.caucho.distcache.ByteStreamCache; import com.caucho.distcache.CacheSerializer; import com.caucho.distcache.ExtCacheEntry; import com.caucho.distcache.ObjectCache; import com.caucho.env.distcache.CacheDataBacking; import com.caucho.loader.Environment; import com.caucho.server.distcache.CacheStoreManager.DataItem; import com.caucho.util.HashKey; import com.caucho.util.L10N; import com.caucho.util.LruCache; import com.caucho.vfs.StreamSource; import com.caucho.vfs.WriteStream; /** * Implements the distributed cache */ public class CacheImpl implements ObjectCache, ByteStreamCache, Closeable { private static final L10N L = new L10N(CacheImpl.class); private CacheManagerImpl _localManager; private CacheStoreManager _manager; private final String _name; private final String _guid; private final CacheConfig _config; private Collection<CacheEntryListener> _listeners = new ConcurrentLinkedQueue<CacheEntryListener>(); private LruCache<Object,DistCacheEntry> _entryCache; private boolean _isInit; private boolean _isClosed; private long _priorMisses = 0; private long _priorHits = 0; public CacheImpl(CacheManagerImpl localManager, String name, String guid, CacheConfig config) { _localManager = localManager; _name = name; _guid = guid; _config = config; init(true); } /** * Returns the name of the cache. */ public String getName() { return _name; } public String getGuid() { return _guid; } /** * The maximum valid time for an item. Items stored in the cache * for longer than the expire time are no longer valid and z null value * will be returned for a get. * <p/> * Default is infinite. */ public long getExpireTimeout() { return _config.getModifiedExpireTimeout(); } /** * The maximum idle time for an item, which is typically used for * temporary data like sessions. For example, session * data might be removed if idle over 30 minutes. * <p/> * Cached data would have infinite idle time because * it doesn't depend on how often it's accessed. * <p/> * Default is infinite. */ public long getIdleTimeout() { return _config.getAccessedExpireTimeout(); } /** * Returns the idle check window, used to minimize traffic when * updating access times. */ public long getIdleTimeoutWindow() { return _config.getAccessedExpireTimeoutWindow(); } /** * The lease timeout is the time a server can use the local version * if it owns it, before a timeout. */ public long getLeaseTimeout() { return _config.getLeaseExpireTimeout(); } /** * The local read timeout is how long a local copy of * a cache item can be reused before checking with the master copy. * <p/> * A read-only item could be infinite (-1). A slow changing item * like a list of bulletin-board comments could be 10s. Even a relatively * quickly changing item can be 10ms or 100ms. * <p/> * The default is 10ms */ public long getLocalReadTimeout() { return _config.getLocalExpireTimeout(); } public CacheImpl createIfAbsent() { init(true); return _localManager.getCache(_guid); } private void init(boolean ifAbsent) { synchronized (this) { if (_isInit) return; _isInit = true; _config.init(); initServer(); if (_config.getEngine() == null) _config.setEngine(_manager.getCacheEngine()); _config.setCacheKey(_manager.createSelfHashKey(_config.getGuid(), _config.getKeySerializer())); _entryCache = new LruCache<Object,DistCacheEntry>(512); Environment.addCloseListener(this); } _manager.initCache(this); } /** * Returns the object with the given key without checking the backing store. */ public Object peek(Object key) { DistCacheEntry cacheEntry = _entryCache.get(key); return (cacheEntry != null) ? cacheEntry.peek() : null; } /** * Returns the hash of the given key */ public HashKey getKeyHash(Object key) { return getDistCacheEntry(key).getKeyHash(); } /** * Returns the object with the given key, checking the backing * store if necessary. */ @Override public Object get(Object key) { DistCacheEntry entry = getDistCacheEntry(key); Object value = entry.get(_config); return value; } /** * Returns the object with the given key, updating the backing * store if necessary. */ /* @Override public Object getLazy(Object key) { return getDistCacheEntry(key).getLazy(_config); } */ /** * Fills an output stream with the value for a key. */ @Override public boolean get(Object key, OutputStream os) throws IOException { return getDistCacheEntry(key).getStream(os, _config); } /** * Returns the cache entry for the object with the given key. */ @Override public ExtCacheEntry getExtCacheEntry(Object key) { return getDistCacheEntry(key).getMnodeValue(_config); } public ExtCacheEntry getExtCacheEntry(HashKey key) { return getDistCacheEntry(key).getMnodeValue(_config); } /** * Returns the cache entry for the object with the given key. */ @Override public ExtCacheEntry peekExtCacheEntry(Object key) { return getDistCacheEntry(key).getMnodeEntry(); } public ExtCacheEntry getStatCacheEntry(Object key) { return getDistCacheEntry(key); } /** * Returns the cache entry for the object with the given key. */ public Cache.Entry getCacheEntry(Object key) { return getExtCacheEntry(key); } /** * Puts a new item in the cache. * * @param key the key of the item to put * @param value the value of the item to put */ @Override public void put(Object key, Object value) { getDistCacheEntry(key).put(value, _config); notifyPut(key); } /** * Puts a new item in the cache with a custom idle * timeout (used for sessions). * * @param key the key of the item to put * @param is the value of the item to put * @param idleTimeout the idle timeout for the item * @param flags the flags value (for memcache) */ @Override public ExtCacheEntry put(Object key, InputStream is, long accessedExpireTimeout, long modifiedExpireTimeout, int flags) throws IOException { return getDistCacheEntry(key).put(is, _config, accessedExpireTimeout, modifiedExpireTimeout, flags); } /** * Puts a new item in the cache with a custom idle * timeout (used for sessions). * * @param key the key of the item to put * @param is the value of the item to put * @param idleTimeout the idle timeout for the item */ @Override public ExtCacheEntry put(Object key, InputStream is, long accessedExpireTimeout, long modifiedExpireTimeout) throws IOException { return getDistCacheEntry(key).put(is, _config, accessedExpireTimeout, modifiedExpireTimeout); } /** * Puts a new item in the cache. * * @param key the key of the item to put * @param value the value of the item to put */ @Override public Object getAndPut(Object key, Object value) { return getDistCacheEntry(key).getAndPut(value, _config); // notifyPut(key); } /** * Updates the cache if the old version matches the current version. * A zero value for the old value hash only adds the entry if it's new. * * @param key the key to compare * @param version the version of the old value, returned by getEntry * @param value the new value * @return true if the update succeeds, false if it fails */ @Override public boolean compareAndPut(Object key, long version, Object value) { put(key, value); return true; } /** * Updates the cache if the old version matches the current value. * A zero value for the old version only adds the entry if it's new. * * @param key the key to compare * @param version the hash of the old version, returned by getEntry * @param inputStream the new value * @return true if the update succeeds, false if it fails */ @Override public boolean compareAndPut(Object key, long version, InputStream inputStream) throws IOException { put(key, inputStream); return true; } public void compareAndPut(HashKey key, HashKey value, long valueLength, long version) { getDistCacheEntry(key).compareAndPut(version, value, valueLength, _config); notifyPut(key); } @Override public boolean putIfAbsent(Object key, Object value) throws CacheException { HashKey NULL = MnodeEntry.NULL_KEY; HashKey result = getDistCacheEntry(key).compareAndPut(NULL, value, _config); return result != null && result.isNull(); } @Override public boolean replace(Object key, Object oldValue, Object value) throws CacheException { DistCacheEntry entry = getDistCacheEntry(key); HashKey oldHash = entry.getValueHash(oldValue, _config); HashKey result = entry.compareAndPut(oldHash, value, _config); return result != null && result.equals(oldHash); } @Override public boolean replace(Object key, Object value) throws CacheException { DistCacheEntry entry = getDistCacheEntry(key); HashKey oldHash = MnodeEntry.ANY_KEY; HashKey result = entry.compareAndPut(oldHash, value, _config); return result != null && ! result.isNull(); } @Override public Object getAndReplace(Object key, Object value) throws CacheException { DistCacheEntry entry = getDistCacheEntry(key); HashKey oldHash = MnodeEntry.ANY_KEY; HashKey result = entry.compareAndPut(oldHash, value, _config); // return result != null && ! result.isNull(); return null; } /** * Removes the entry from the cache. * * @return true if the object existed */ @Override public boolean remove(Object key) { notifyRemove(key); getDistCacheEntry(key).remove(_config); return true; } /** * Removes the entry from the cache. * * @return true if the object existed */ @Override public boolean remove(Object key, Object oldValue) { notifyRemove(key); getDistCacheEntry(key).remove(_config); return true; } @Override public Object getAndRemove(Object key) throws CacheException { notifyRemove(key); return getDistCacheEntry(key).remove(_config); } /** * Removes the entry from the cache if the current entry matches the version. */ @Override public boolean compareAndRemove(Object key, long version) { DistCacheEntry cacheEntry = getDistCacheEntry(key); if (cacheEntry.getVersion() == version) { remove(key); return true; } return false; } /** * Returns the entry for the given key, returning the live item. */ public ExtCacheEntry getLiveCacheEntry(Object key) { return getDistCacheEntry(key); } /** * Returns the CacheKeyEntry for the given key. */ protected DistCacheEntry getDistCacheEntry(Object key) { DistCacheEntry cacheEntry = null; // cacheEntry = _entryCache.get(key); if (cacheEntry == null) { cacheEntry = _manager.getCacheEntry(key, _config); // _entryCache.put(key, cacheEntry); } return cacheEntry; } /** * Returns the CacheKeyEntry for the given key. */ protected DistCacheEntry getDistCacheEntry(HashKey key) { return _manager.getCacheEntry(key, _config); } /** * Returns a new map of the items found in the central cache. * * @note If a cacheLoader is configured if an item is not found in the cache. */ public Map getAll(Collection keys) { Map result = new HashMap(); for (Object key : keys) { Object value = get(key); if (value != null) { result.put(key, value); } } return result; } /** * Loads an item into the cache if not already there and was returned from in the optional cache loader. * * @param key */ /* @Override public void load(Object key) { if (containsKey(key) || get(key) != null) return; Object loaderValue = cacheLoader(key); if (loaderValue != null) put(key, loaderValue); notifyLoad(key); } */ /** * Implements the loadAll method for a collection of keys. */ /* public void loadAll(Collection keys) { Map<Object, Object> entries = null; CacheLoader loader = _config.getCacheLoader(); if (loader == null || keys == null || keys.size() == 0) return; entries = loader.loadAll(keys); if (entries.isEmpty()) return; for (Entry loaderEntry : entries.entrySet()) { Object loaderKey = loaderEntry.getKey(); if (!containsKey(loaderKey) && (get(loaderKey) != null)) { put(loaderKey, loaderEntry.getValue()); notifyLoad(loaderKey); } } } */ /** * Adds a listener to the cache. */ @Override public boolean registerCacheEntryListener(CacheEntryListener listener, NotificationScope scope, boolean synchronous) { _listeners.add(listener); return true; } /** * Removes a listener from the cache. */ @Override public boolean unregisterCacheEntryListener(CacheEntryListener listener) { _listeners.remove(listener); return true; } /** * Returns the CacheStatistics for this cache. */ @Override public CacheStatistics getStatistics() { return null; // this; } /** * Puts each item in the map into the cache. */ @Override public void putAll(Map map) { if (map == null || map.size() == 0) return; Set entries = map.entrySet(); for (Object item : entries) { Map.Entry entry = (Map.Entry) item; put(entry.getKey(), entry.getValue()); } } /** * Returns true if an entry for the item is found in the cache. * * @param key * @return */ @Override public boolean containsKey(Object key) { return _entryCache.get(key) != null; } public boolean isBackup() { return _config.isBackup(); } public boolean isTriplicate() { return _config.isTriplicate(); } public HashKey getCacheKey() { return _config.getCacheKey(); } /** * Places an item in the cache from the loader unless the item is in cache already. */ protected Object cacheLoader(Object key) { Object value = get(key); if (value != null) return value; CacheLoader loader = _config.getCacheLoader(); Object arg = null; value = (loader != null) ? loader.load(key) : null; if (value != null) put(key, value); notifyLoad(key); return value; } protected void notifyLoad(Object key) { /* for (CacheEntryListener listener : _listeners) { listener.onLoad(key); } */ } protected void notifyEvict(Object key) { /* for (CacheEntryListener listener : _listeners) { listener.onEvict(key); } */ } protected void notifyClear(Object key) { /* for (CacheEntryListener listener : _listeners) { listener.onClear(); } */ } protected void notifyPut(Object key) { /* for (CacheEntryListener listener : _listeners) { listener.onPut(key); } */ } protected void notifyRemove(Object key) { /* for (CacheEntryListener listener : _listeners) { listener.onRemove(key); } */ } @Override public boolean isClosed() { return _isClosed; } @Override public void close() { _isClosed = true; _localManager.remove(_guid); _manager.closeCache(_guid); } public boolean loadData(HashKey valueHash, WriteStream os) throws IOException { return getDataBacking().loadData(valueHash, os); } public boolean saveData(HashKey valueHash, StreamSource source, int length) throws IOException { return getDataBacking().saveData(valueHash, source, length); } public boolean isDataAvailable(HashKey valueKey) { return getDataBacking().isDataAvailable(valueKey); } private CacheDataBacking getDataBacking() { return _manager.getDataBacking(); } // // QA // public byte []getKeyHash(String name) { return getDistCacheEntry(name).getKeyHash().getHash(); } public byte []getValueHash(Object value) { return _manager.calculateValueHash(value, _config); } @SuppressWarnings("unchecked") public MnodeStore getMnodeStore() { return ((CacheStoreManager) _manager).getMnodeStore(); } @SuppressWarnings("unchecked") public DataStore getDataStore() { return ((CacheStoreManager) _manager).getDataStore(); } public void saveData(Object value) { ((CacheStoreManager) _manager).writeData(null, value, _config.getValueSerializer()); } public HashKey saveData(InputStream is) throws IOException { DataItem dataItem = ((CacheStoreManager) _manager).writeData(null, is); return dataItem.getValue(); } public void setManager(CacheStoreManager manager) { if (_manager != null) throw new IllegalStateException(); _manager = manager; } private void initServer() throws ConfigException { if (_manager != null) return; DistCacheSystem cacheService = DistCacheSystem.getCurrent(); if (cacheService == null) throw new ConfigException(L.l("'{0}' cannot be initialized because it is not in a Resin environment", getClass().getSimpleName())); _manager = cacheService.getDistCacheManager(); if (_manager == null) throw new IllegalStateException("distributed cache manager not available"); } /** * Provides an iterator over the entries in the the local cache. */ protected static class CacheEntrySetIterator<K, V> implements Iterator<Cache.Entry<K, V>> { private Iterator<LruCache.Entry<K, V>> _iterator; protected CacheEntrySetIterator(LruCache<K, V> lruCache) { _iterator = lruCache.iterator(); } public Cache.Entry<K,V> next() { if (!hasNext()) throw new NoSuchElementException(); LruCache.Entry<K, V> entry = _iterator.next(); Cache.Entry<K,V> cacheEntry = (Cache.Entry<K, V>) entry.getValue(); return new EntryImpl<K, V>(cacheEntry.getKey(), cacheEntry.getValue()); } public boolean hasNext() { return _iterator.hasNext(); } /** * */ public void remove() { throw new UnsupportedOperationException(getClass().getName()); } } @Override public String toString() { return getClass().getSimpleName() + "[" + _guid + "]"; } static class EntryImpl<K,V> implements Cache.Entry<K,V> { private final K _key; private final V _value; EntryImpl(K key, V value) { _key = key; _value = value; } @Override public K getKey() { return _key; } @Override public V getValue() { return _value; } } /* (non-Javadoc) * @see javax.cache.Cache#getConfiguration() */ @Override public CacheConfiguration getConfiguration() { // TODO Auto-generated method stub return null; } @Override public CacheManager getCacheManager() { return null; } /* (non-Javadoc) * @see javax.cache.Cache#load(java.lang.Object, javax.cache.CacheLoader, java.lang.Object) */ @Override public Future load(Object key) throws CacheException { // TODO Auto-generated method stub return null; } /* (non-Javadoc) * @see javax.cache.Cache#loadAll(java.util.Collection, javax.cache.CacheLoader, java.lang.Object) */ @Override public Future loadAll(Collection keys) throws CacheException { // TODO Auto-generated method stub return null; } /* (non-Javadoc) * @see javax.cache.Cache#removeAll(java.util.Collection) */ @Override public void removeAll(Collection keys) throws CacheException { // TODO Auto-generated method stub } /* (non-Javadoc) * @see javax.cache.Cache#removeAll() */ @Override public void removeAll() throws CacheException { // TODO Auto-generated method stub } /* (non-Javadoc) * @see java.lang.Iterable#iterator() */ @Override public Iterator iterator() { // TODO Auto-generated method stub return null; } /* (non-Javadoc) * @see javax.cache.Lifecycle#getStatus() */ @Override public Status getStatus() { // TODO Auto-generated method stub return null; } /* (non-Javadoc) * @see javax.cache.Lifecycle#start() */ @Override public void start() throws CacheException { // TODO Auto-generated method stub } /* (non-Javadoc) * @see javax.cache.Lifecycle#stop() */ @Override public void stop() throws CacheException { // TODO Auto-generated method stub } /* (non-Javadoc) * @see javax.cache.Cache#unwrap(java.lang.Class) */ @Override public Object unwrap(Class cl) { // TODO Auto-generated method stub return null; } }