/* * Copyright (C) 2009 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.impl.memcached; import net.spy.memcached.CASValue; import net.spy.memcached.MemcachedClient; import net.spy.memcached.internal.OperationFuture; import org.apache.ws.commons.util.Base64; import org.exoplatform.container.ExoContainerContext; import org.exoplatform.management.annotations.Managed; import org.exoplatform.management.annotations.ManagedDescription; import org.exoplatform.management.annotations.ManagedName; import org.exoplatform.services.cache.CacheInfo; 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.ExoCacheConfig; import org.exoplatform.services.log.ExoLogger; import org.exoplatform.services.log.Log; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; /** * An {@link org.exoplatform.services.cache.ExoCache} implementation based on spymemcached. * * @author <a href="mailto:nfilotto@exoplatform.com">Nicolas Filotto</a> * @version $Id$ */ public class MCExoCache<K extends Serializable, V> implements ExoCache<K, V> { /** * Logger. */ private static final Log LOG = ExoLogger//NOSONAR .getLogger("exo.kernel.component.ext.cache.impl.memcached.v1.AbstractExoCache");//NOSONAR private final AtomicInteger hits = new AtomicInteger(0); private final AtomicInteger misses = new AtomicInteger(0); private final AtomicInteger count = new AtomicInteger(0); private final AtomicReference<String> lastNamespace = new AtomicReference<String>(); private String label; private String name; private final String fullName; private boolean distributed; private boolean replicated; private boolean logEnabled; private int expirationTimeout; protected final MemcachedClient cache; @SuppressWarnings("rawtypes") private static final ConcurrentMap<String, List<ListenerContext>> ALL_LISTENERS = new ConcurrentHashMap<String, List<ListenerContext>>(); public MCExoCache(ExoContainerContext ctx, ExoCacheConfig config, MemcachedClient cache, long expirationTimeout) { this.fullName = ctx.getName() + "-" + config.getName(); this.cache = cache; this.expirationTimeout = (int)(expirationTimeout / 1000L); setDistributed(config.isDistributed()); setLabel(config.getLabel()); setName(config.getName()); setLogEnabled(config.isLogEnabled()); setReplicated(config.isRepicated()); } /** * @return the fullName */ String getFullName() { return fullName; } /** * {@inheritDoc} */ @SuppressWarnings("rawtypes") public void addCacheListener(CacheListener<? super K, ? super V> listener) { if (listener == null) { throw new IllegalArgumentException("The listener cannot be null"); } List<ListenerContext> lListeners = getOrCreateListeners(); lListeners.add(new ListenerContext<K, V>(listener, this)); } @SuppressWarnings("rawtypes") private List<ListenerContext> getOrCreateListeners() { List<ListenerContext> lListeners = getListeners(); if (lListeners == null) { lListeners = new CopyOnWriteArrayList<ListenerContext>(); List<ListenerContext> oldValue = ALL_LISTENERS.putIfAbsent(fullName, lListeners); if (oldValue != null) { lListeners = oldValue; } } return lListeners; } @SuppressWarnings("rawtypes") private List<ListenerContext> getListeners() { return ALL_LISTENERS.get(fullName); } /** * Tries at worse 3 times to get the namespace * @return */ private String getNamespace() { return getNamespace(3); } /** * Gives the namespace to use as prefix for our key in order to allow invalidation of a cache * @param triesLeft the total amount of tries left in case of a failure * @return the namespace */ private String getNamespace(int triesLeft) { String oldNamespace = lastNamespace.get(); CASValue<Object> casValue = cache.getAndTouch(fullName, expirationTimeout); String value; if (casValue == null || casValue.getValue() == null) { value = UUID.randomUUID().toString(); OperationFuture<Boolean> resp = cache.add(fullName, expirationTimeout, value); Boolean result = null; try { result = resp.get(); } catch (InterruptedException e) { LOG.error("Could not get the namespace", e); } catch (ExecutionException e) { LOG.error("Could not get the namespace", e); } if (result == null || !result.booleanValue()) { if (result == null && triesLeft == 0) { throw new RuntimeException("The namespace could not be found"); } LOG.debug("Could not get the namespace, so we need to retry"); return getNamespace(triesLeft - 1); } } else { value = (String)casValue.getValue(); } if (lastNamespace.compareAndSet(oldNamespace, value) && oldNamespace != null && !oldNamespace.equals(value)) { // The namespace has changed so we reset the counter as it could be due to // a remote clear cache count.set(0); } return value; } /** * Gives the name of the key with the prefix * @param name the name of the key without the prefix * @return the full name of the key */ private String getKeyFullName(Serializable name) { return getKeyFullName(getNamespace(), name); } /** * Gives the name of the key with the prefix * * @param namespace the namespace to use * @param name the name of the key without the prefix * @return the full name of the key */ private String getKeyFullName(String namespace, Serializable name) { StringBuilder sb = new StringBuilder(); sb.append(namespace); sb.append(':'); sb.append(toString(name)); return sb.toString(); } /** * Used to serialize the key * @param key the key to serialize * @return the value of the key serialized in Base64 */ private static String toString(Serializable key) { if (key instanceof String) return (String)key; ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = null; try { oos = new ObjectOutputStream(baos); oos.writeObject(key); } catch (IOException e) { throw new RuntimeException("Could not serialize the key " + key, e); } finally { if (oos != null) { try { oos.close(); } catch (IOException e) { LOG.trace("Could not close the object output stream", e); } } } return Base64.encode(baos.toByteArray()); } /** * {@inheritDoc} */ public void clearCache() { // As it is not possible to clear a particular cache, we simply change the namespace as // described in the doc https://code.google.com/p/memcached/wiki/NewProgrammingTricks#Namespacing String namespace = UUID.randomUUID().toString(); String oldNamespace = lastNamespace.get(); OperationFuture<Boolean> resp = cache.set(fullName, expirationTimeout, namespace); Boolean result; try { result = resp.get(); } catch (Exception e) { throw new RuntimeException("Could not clear the cache ", e); } if (result != null && result.booleanValue()) { lastNamespace.compareAndSet(oldNamespace, namespace); count.set(0); onClearCache(); } } /** * {@inheritDoc} */ @SuppressWarnings("unchecked") public V get(Serializable name) { if (name == null) { return null; } CASValue<Object> casValue = cache.getAndTouch(getKeyFullName(name), expirationTimeout); V result = casValue == null ? null : (V)casValue.getValue(); if (result == null) { misses.incrementAndGet(); } else { hits.incrementAndGet(); } onGet((K)name, result); return result; } /** * {@inheritDoc} */ public int getCacheHit() { return hits.get(); } /** * {@inheritDoc} */ public int getCacheMiss() { return misses.get(); } /** * {@inheritDoc} */ @Managed @ManagedName("Size") @ManagedDescription("The local cache size as it is not possible to get the global cache size") public int getCacheSize() { return count.get(); } /** * {@inheritDoc} */ public List<V> getCachedObjects() { throw new UnsupportedOperationException("Cannot get the cached objects"); } /** * {@inheritDoc} */ public String getLabel() { return label; } /** * {@inheritDoc} */ public String getName() { return name; } /** * {@inheritDoc} */ public boolean isDistributed() { return distributed; } /** * {@inheritDoc} */ public boolean isLogEnabled() { return logEnabled; } /** * {@inheritDoc} */ public boolean isReplicated() { return replicated; } /** * {@inheritDoc} */ public void put(final K key, final V value) throws IllegalArgumentException { if (key == null) { throw new IllegalArgumentException("No null cache key accepted"); } else if (value == null) { // ignore null values return; } putOnly(getNamespace(), key, value); onPut(key, value); } /** * Only puts the data into the cache nothing more */ protected void putOnly(String namespace, K key, V value) { OperationFuture<Boolean> resp = cache.add(getKeyFullName(namespace, key), expirationTimeout, value); Boolean result; try { result = resp.get(); } catch (Exception e) { throw new RuntimeException("Could not add the new value for the key " + key, e); } if (result == null || !result.booleanValue()) { // The value already exists in the cache so we simply replace it resp = cache.replace(getKeyFullName(namespace, key), expirationTimeout, value); try { result = resp.get(); } catch (Exception e) { throw new RuntimeException("Could not replace the old value of the key " + key, e); } if (result == null || !result.booleanValue()) { // we try again putOnly(namespace, key, value); } } else if (namespace.equals(lastNamespace.get())) { // A new value has been added and the namespace has not been modified during the process count.incrementAndGet(); } } /** * {@inheritDoc} */ public void putMap(final Map<? extends K, ? extends V> objs) throws IllegalArgumentException { if (objs == null) { throw new IllegalArgumentException("No null map accepted"); } for (Serializable name : objs.keySet()) { if (name == null) { throw new IllegalArgumentException("No null cache key accepted"); } } try { String namespace = getNamespace(); for (Map.Entry<? extends K, ? extends V> entry : objs.entrySet()) { putOnly(namespace, entry.getKey(), entry.getValue()); onPut(entry.getKey(), entry.getValue()); } } catch (Exception e)//NOSONAR { LOG.warn("An error occurs while executing the putMap method", e); } } /** * {@inheritDoc} */ @SuppressWarnings("unchecked") public V remove(Serializable name) throws IllegalArgumentException { if (name == null) { throw new IllegalArgumentException("No null cache key accepted"); } String namespace = getNamespace(); V value = (V)cache.get(getKeyFullName(namespace, name)); OperationFuture<Boolean> resp = cache.delete(getKeyFullName(namespace, name)); Boolean result; try { result = resp.get(); } catch (Exception e) { throw new RuntimeException("Could not remove the value for the key " + name, e); } if (result != null && result.booleanValue()) { if (namespace.equals(lastNamespace.get())) { // The value has been removed successfully and the namespace has not been modified during the process count.decrementAndGet(); } onRemove((K)name, value); } return value; } /** * {@inheritDoc} */ public List<V> removeCachedObjects() { final List<V> list = getCachedObjects(); clearCache(); return list; } /** * {@inheritDoc} */ public void select(CachedObjectSelector<? super K, ? super V> selector) throws Exception { throw new UnsupportedOperationException("Cannot select a sub part of the cache dynamically"); } /** * {@inheritDoc} */ public void setDistributed(boolean distributed) { this.distributed = distributed; } /** * {@inheritDoc} */ public void setLabel(String label) { this.label = label; } /** * {@inheritDoc} */ public void setLogEnabled(boolean logEnabled) { this.logEnabled = logEnabled; } /** * {@inheritDoc} */ public void setName(String name) { this.name = name; } /** * {@inheritDoc} */ public void setReplicated(boolean replicated) { this.replicated = replicated; } @SuppressWarnings({"rawtypes", "unchecked"}) void onExpire(K key, V obj) { List<ListenerContext> listeners = getListeners(); if (listeners == null || listeners.isEmpty()) { return; } for (ListenerContext context : listeners) { try { context.onExpire(key, obj); } catch (Exception e)//NOSONAR { if (LOG.isWarnEnabled()) LOG.warn("Cannot execute the CacheListener properly", e); } } } @SuppressWarnings({"rawtypes", "unchecked"}) void onRemove(K key, V obj) { List<ListenerContext> listeners = getListeners(); if (listeners == null || listeners.isEmpty()) { return; } for (ListenerContext context : listeners) { try { context.onRemove(key, obj); } catch (Exception e)//NOSONAR { if (LOG.isWarnEnabled()) LOG.warn("Cannot execute the CacheListener properly", e); } } } @SuppressWarnings({"rawtypes", "unchecked"}) void onPut(K key, V obj) { List<ListenerContext> listeners = getListeners(); if (listeners == null || listeners.isEmpty()) { return; } for (ListenerContext context : listeners) { try { context.onPut(key, obj); } catch (Exception e)//NOSONAR { if (LOG.isWarnEnabled()) LOG.warn("Cannot execute the CacheListener properly", e); } } } @SuppressWarnings({"rawtypes", "unchecked"}) void onGet(K key, V obj) { List<ListenerContext> listeners = getListeners(); if (listeners == null || listeners.isEmpty()) { return; } for (ListenerContext context : listeners) { try { context.onGet(key, obj); } catch (Exception e)//NOSONAR { if (LOG.isWarnEnabled()) LOG.warn("Cannot execute the CacheListener properly", e); } } } @SuppressWarnings("rawtypes") void onClearCache() { List<ListenerContext> listeners = getListeners(); if (listeners == null || listeners.isEmpty()) { return; } for (ListenerContext context : listeners) { try { context.onClearCache(); } catch (Exception e)//NOSONAR { if (LOG.isWarnEnabled()) LOG.warn("Cannot execute the CacheListener properly", e); } } } private static class ListenerContext<K extends Serializable, V> implements CacheListenerContext, CacheInfo { /** . */ private final ExoCache<K, V> cache; /** . */ final CacheListener<? super K, ? super V> listener; public ListenerContext(CacheListener<? super K, ? super V> listener, ExoCache<K, V> cache) { this.listener = listener; this.cache = cache; } public CacheInfo getCacheInfo() { return this; } public String getName() { return cache.getName(); } public int getMaxSize() { return cache.getMaxSize(); } public long getLiveTime() { return cache.getLiveTime(); } public int getSize() { return cache.getCacheSize(); } void onExpire(K key, V obj) throws Exception { listener.onExpire(this, key, obj); } void onRemove(K key, V obj) throws Exception { listener.onRemove(this, key, obj); } void onPut(K key, V obj) throws Exception { listener.onPut(this, key, obj); } void onGet(K key, V obj) throws Exception { listener.onGet(this, key, obj); } void onClearCache() throws Exception { listener.onClearCache(this); } } @Managed @ManagedName("ExpirationTimeout") @ManagedDescription("This is the timeout after which the cache entry must be evicted.") public long getExpirationTimeout() { return expirationTimeout; } @Managed public void setExpirationTimeout(long expirationTimeout) { this.expirationTimeout = (int)(expirationTimeout / 1000L); } public void setMaxSize(int max) { throw new UnsupportedOperationException("The max size cannot be modified"); } public void setLiveTime(long period) { this.expirationTimeout = (int)period; } public int getMaxSize() { return -1; } public long getLiveTime() { return expirationTimeout; } }