/* * Copyright (C) 2011 eXo Platform SAS. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software 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. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.exoplatform.services.cache.invalidation; import org.exoplatform.services.cache.CacheListener; import org.exoplatform.services.cache.CacheListenerContext; import org.exoplatform.services.cache.CachedObjectSelector; import org.exoplatform.services.cache.ExoCache; import org.exoplatform.services.cache.ObjectCacheInfo; import org.exoplatform.services.log.ExoLogger; import org.exoplatform.services.log.Log; import java.io.Externalizable; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; import java.io.Serializable; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; /** * This eXo cache type is a decorator allowing ExoCache instances that have * big values or non serializable values to be replicated thanks to an invalidation * mechanism. To prevent infinite loop described below, we replicate the hash code of * the value such that if the hash code is the same, we don't invalidate the value locally * <ul> * <li>Cluster node #1 puts (key1, value1) into the cache</li> * <li>On cluster node #2 key1 is invalidated by the put call in node #1</li> * <li>Node #2 re-loads key1 and puts (key1, value1) into the cache</li> * <li>On cluster node #1 key1 is invalidated, so we get back to step #1</li> * </ul> * * @author <a href="mailto:nfilotto@exoplatform.com">Nicolas Filotto</a> * @version $Id$ * */ public class InvalidationExoCache<K extends Serializable, V> implements ExoCache<K, V>, CacheListener<K, InvalidationExoCache.HashCode<V>> { /** * Logger. */ private static final Log LOG = ExoLogger.getLogger("exo.kernel.component.cache.InvalidationExoCache"); /** * The eXo cache instance that we would like to replicate using the invalidation * mechanism */ private final ExoCache<K, HashCode<V>> delegate; /** * The listeners of the cache */ private final CopyOnWriteArrayList<CacheListener<? super K, ? super V>> listeners; /** * The local cache that contains the real values */ private final ConcurrentMap<K, V> localCache; /** * @param delegate the underneath eXo cache instance, we assume that the eXo cache * implementation behind is fully functional. */ public InvalidationExoCache(ExoCache<K, V> delegate) { this(delegate, delegate.getMaxSize() > 0 && delegate.getMaxSize() < 512 ? delegate.getMaxSize() : 512); } /** * @param delegate the underneath eXo cache instance, we assume that the eXo cache * implementation behind is fully functional. * @param concurrencyLevel the estimated number of concurrently * updating threads. The implementation performs internal sizing * to try to accommodate this many threads. */ @SuppressWarnings("unchecked") public InvalidationExoCache(ExoCache<K, V> delegate, int concurrencyLevel) { this.delegate = (ExoCache<K, HashCode<V>>)delegate; // We listen to the cache in order to get a callbacks in case of internal puts for example this.delegate.addCacheListener(this); this.listeners = new CopyOnWriteArrayList<CacheListener<? super K, ? super V>>(); this.localCache = new ConcurrentHashMap<K, V>(concurrencyLevel, 0.75f, concurrencyLevel); } /** * @see org.exoplatform.services.cache.ExoCache#getName() */ public String getName() { return delegate.getName(); } /** * @see org.exoplatform.services.cache.ExoCache#setName(java.lang.String) */ public void setName(String name) { delegate.setName(name); } /** * @see org.exoplatform.services.cache.ExoCache#getLabel() */ public String getLabel() { return delegate.getLabel(); } /** * @see org.exoplatform.services.cache.ExoCache#setLabel(java.lang.String) */ public void setLabel(String s) { delegate.setLabel(s); } /** * @see org.exoplatform.services.cache.ExoCache#get(java.io.Serializable) */ public V get(Serializable name) { HashCode<V> result = delegate.get(name); return result == null ? null : localCache.get(name); } /** * @see org.exoplatform.services.cache.ExoCache#remove(java.io.Serializable) */ public V remove(Serializable key) throws NullPointerException { V value = localCache.get(key); delegate.remove(key); return value; } /** * @see org.exoplatform.services.cache.ExoCache#put(java.io.Serializable, java.lang.Object) */ public void put(K key, V value) throws NullPointerException { delegate.put(key, new HashCode<V>(value)); } /** * @see org.exoplatform.services.cache.ExoCache#putMap(java.util.Map) */ public void putMap(Map<? extends K, ? extends V> objs) throws IllegalArgumentException { if (objs == null) { throw new IllegalArgumentException("No null map accepted"); } Map<K, HashCode<V>> map = new LinkedHashMap<K, HashCode<V>>(); for (Entry<? extends K, ? extends V> entry : objs.entrySet()) { if (entry.getKey() == null) { throw new IllegalArgumentException("No null cache key accepted"); } else if (entry.getValue() == null) { throw new IllegalArgumentException("No null cache value accepted"); } map.put(entry.getKey(), new HashCode<V>(entry.getValue())); } delegate.putMap(map); } /** * @see org.exoplatform.services.cache.ExoCache#clearCache() */ public void clearCache() { delegate.clearCache(); } /** * @see org.exoplatform.services.cache.ExoCache#select(org.exoplatform.services.cache.CachedObjectSelector) */ public void select(CachedObjectSelector<? super K, ? super V> selector) throws Exception { if (selector == null) { throw new IllegalArgumentException("No null selector"); } for (Entry<K, V> entry : localCache.entrySet()) { final K key = entry.getKey(); final V value = entry.getValue(); ObjectCacheInfo<V> info = new ObjectCacheInfo<V>() { public V get() { return value; } public long getExpireTime() { // Cannot know: The expire time is managed by eXo Cache itself return -1; } }; if (selector.select(key, info)) { selector.onSelect(this, key, info); } } } /** * @see org.exoplatform.services.cache.ExoCache#getCacheSize() */ public int getCacheSize() { return localCache.size(); } /** * @see org.exoplatform.services.cache.ExoCache#getMaxSize() */ public int getMaxSize() { return delegate.getMaxSize(); } /** * @see org.exoplatform.services.cache.ExoCache#setMaxSize(int) */ public void setMaxSize(int max) { delegate.setMaxSize(max); } /** * @see org.exoplatform.services.cache.ExoCache#getLiveTime() */ public long getLiveTime() { return delegate.getLiveTime(); } /** * @see org.exoplatform.services.cache.ExoCache#setLiveTime(long) */ public void setLiveTime(long period) { delegate.setLiveTime(period); } /** * @see org.exoplatform.services.cache.ExoCache#getCacheHit() */ public int getCacheHit() { return delegate.getCacheHit(); } /** * @see org.exoplatform.services.cache.ExoCache#getCacheMiss() */ public int getCacheMiss() { return delegate.getCacheMiss(); } /** * @see org.exoplatform.services.cache.ExoCache#getCachedObjects() */ public List<? extends V> getCachedObjects() { return new ArrayList<V>(localCache.values()); } /** * @see org.exoplatform.services.cache.ExoCache#removeCachedObjects() */ public List<? extends V> removeCachedObjects() { final List<? extends V> list = getCachedObjects(); clearCache(); return list; } /** * @see org.exoplatform.services.cache.ExoCache#addCacheListener(org.exoplatform.services.cache.CacheListener) */ public void addCacheListener(CacheListener<? super K, ? super V> listener) throws IllegalArgumentException { if (listener == null) { throw new IllegalArgumentException("The listener cannot be null"); } listeners.add(listener); } /** * @see org.exoplatform.services.cache.ExoCache#isLogEnabled() */ public boolean isLogEnabled() { return delegate.isLogEnabled(); } /** * @see org.exoplatform.services.cache.ExoCache#setLogEnabled(boolean) */ public void setLogEnabled(boolean b) { delegate.setLogEnabled(b); } /** * {@inheritDoc} */ public void onExpire(CacheListenerContext context, K key, HashCode<V> obj) throws Exception { V value = localCache.remove(key); if (listeners.isEmpty()) { return; } for (CacheListener<? super K, ? super V> listener : listeners) { try { listener.onExpire(context, key, value); } catch (Exception e) { if (LOG.isWarnEnabled()) LOG.warn("Cannot execute the CacheListener properly", e); } } } /** * {@inheritDoc} */ public void onRemove(CacheListenerContext context, K key, HashCode<V> obj) throws Exception { V value = localCache.remove(key); if (listeners.isEmpty()) { return; } for (CacheListener<? super K, ? super V> listener : listeners) { try { listener.onRemove(context, key, value); } catch (Exception e) { if (LOG.isWarnEnabled()) LOG.warn("Cannot execute the CacheListener properly", e); } } } /** * {@inheritDoc} */ public void onPut(CacheListenerContext context, K key, HashCode<V> obj) throws Exception { V value = obj == null ? null : obj.getValue(); if (value != null) { // we assume that it is a local put since the value is inside the HashCode object localCache.put(key, value); } else { // we assume that it is a remote put since the value is not inside the HashCode object V currentValue = localCache.get(key); if (currentValue != null && obj != null && currentValue.hashCode() == obj.hashCode()) { // We assume that it is the same value so we don't change the value in the cache value = currentValue; } else { // A new value has been added to the cache so we invalidate the local one value = null; localCache.remove(key); } } if (listeners.isEmpty()) { return; } for (CacheListener<? super K, ? super V> listener : listeners) try { listener.onPut(context, key, value); } catch (Exception e) { if (LOG.isWarnEnabled()) LOG.warn("Cannot execute the CacheListener properly", e); } } /** * {@inheritDoc} */ public void onGet(CacheListenerContext context, K key, HashCode<V> obj) throws Exception { if (listeners.isEmpty()) { return; } V value = obj == null ? null : localCache.get(key); for (CacheListener<? super K, ? super V> listener : listeners) try { listener.onGet(context, key, value); } catch (Exception e) { if (LOG.isWarnEnabled()) LOG.warn("Cannot execute the CacheListener properly", e); } } /** * @see org.exoplatform.services.cache.CacheListener#onClearCache(org.exoplatform.services.cache.CacheListenerContext) */ public void onClearCache(CacheListenerContext context) throws Exception { localCache.clear(); if (listeners.isEmpty()) { return; } for (CacheListener<? super K, ? super V> listener : listeners) { try { listener.onClearCache(context); } catch (Exception e) { if (LOG.isWarnEnabled()) LOG.warn("Cannot execute the CacheListener properly", e); } } } /** * We use this class to propagate the hash code of the value efficiently over the network */ public static class HashCode<V> implements Externalizable { /** * The hash code of the value */ private int hashCode; /** * The corresponding value */ private V value; public HashCode() {} public HashCode(V value) { this.hashCode = value.hashCode(); this.value = value; } /** * @return the value */ public V getValue() { return value; } /** * @see java.io.Externalizable#writeExternal(java.io.ObjectOutput) */ public void writeExternal(ObjectOutput out) throws IOException { out.writeInt(hashCode); } /** * @see java.io.Externalizable#readExternal(java.io.ObjectInput) */ public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { this.hashCode = in.readInt(); } /** * @see java.lang.Object#hashCode() */ @Override public int hashCode() { return hashCode; } /** * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; @SuppressWarnings("rawtypes") HashCode other = (HashCode)obj; if (hashCode != other.hashCode) return false; if (value != null && other.value != null) { return value.equals(other.value); } return true; } /** * @see java.lang.Object#toString() */ @Override public String toString() { return "HashCode [hashCode=" + hashCode + ", value=" + value + "]"; } } }