/* * Copyright (C) 2014 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.concurrent; import org.exoplatform.container.ExoContainer; import org.exoplatform.container.ExoContainerContext; import org.exoplatform.services.cache.ExoCache; import org.exoplatform.services.log.ExoLogger; import org.exoplatform.services.log.Log; import org.exoplatform.services.rpc.RPCService; import org.exoplatform.services.rpc.RemoteCommand; import java.io.Serializable; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.UUID; /** * <p>This implementation of {@link ExoCache} will behave exactly the same way as {@link ConcurrentFIFOExoCache} * except in case of a cache change, indeed the modifications will be first applied locally * then it will be replicated over the cluster asynchronously to limit the performance impact * on the local cluster node.</p> * <p>This class can be used as a drop-in replacement for {@link ConcurrentFIFOExoCache} in a cluster environment * as long as we know that modifications like remove, clearCache, removeCachedObjects, put or putMap happen rarely. * In other words, it should be used for caches that rarely change.</p> * * <b>This limitation is due to the fact that the mechanism used for the replication * has not been designed to support heavy load so it must be used with a lot of caution.</b> * * @author <a href="mailto:nfilotto@exoplatform.com">Nicolas Filotto</a> * @version $Id$ * */ public class SimpleReplicatedExoCache<K extends Serializable, V extends Serializable> extends ConcurrentFIFOExoCache<K, V> { /** * Logger. */ private static final Log LOG = ExoLogger.getLogger("exo.kernel.component.cache.SimpleReplicatedExoCache"); /** * Component used to execute commands over the cluster. */ private final RPCService rpcService; /** * The generic command used to replicate changes over the cluster */ private RemoteCommand command; /** * Id used to avoid launching twice the same command on the same node */ private final String id; /** * The name of the current context */ private final String ctxName; public SimpleReplicatedExoCache() { ExoContainer container = ExoContainerContext.getCurrentContainer(); this.rpcService = container.getComponentInstanceOfType(RPCService.class); if (rpcService == null) throw new IllegalArgumentException("The RPCService is required for this type of cache, please configure it first"); this.ctxName = container.getContext().getName(); this.id = UUID.randomUUID().toString(); } SimpleReplicatedExoCache(ExoContainerContext ctx, RPCService rpcService) { if (rpcService == null) throw new IllegalArgumentException("The RPCService is required for this type of cache, please configure it first"); this.rpcService = rpcService; this.ctxName = ctx.getName(); this.id = UUID.randomUUID().toString(); } @Override public void setName(String s) { super.setName(s); if (command == null) { command = rpcService.registerCommand(new RemoteCommand() { private final String commandId = SimpleReplicatedExoCache.class.getName() + "-" + getName() + "-" + ctxName; public String getId() { return commandId; } @SuppressWarnings("unchecked") public Serializable execute(Serializable[] args) throws Throwable { if (!id.equals(args[0])) { if ("c".equals(args[1])) { try { clearCacheOnly(); } catch (Exception e) { LOG.warn("Could not clear the cache on other cluster nodes", e); } } else if ("r".equals(args[1])) { try { removeOnly((Serializable)args[2]); } catch (Exception e) { LOG.warn("Could not remove the entry " + args[2] + " on other cluster nodes", e); } } else if ("p".equals(args[1])) { try { putIfNeeded((K)args[2], (V)args[3]); } catch (Exception e) { LOG.warn("Could not put the entry " + args[2] + " on other cluster nodes", e); } } else if ("m".equals(args[1])) { try { Map<? extends K, ? extends V> objs = (Map<? extends K, ? extends V>)args[2]; for (Entry<? extends K, ? extends V> entry : objs.entrySet()) { putIfNeeded(entry.getKey(), entry.getValue()); } } catch (Exception e) { LOG.warn("Could not put entries on other cluster nodes", e); } } } return true; } }); } } /** * Removes the entry without replication * @param name the key of the entry to remove */ void removeOnly(Serializable name) { super.remove(name); } /** * Clears the cache without replication */ void clearCacheOnly() { super.clearCache(); } /** * Puts the entry without replication only if the current cache * doesn't have already the exact same entry with the same key and value * @param name the key of the entry to put * @param obj the value of the entry to put */ void putIfNeeded(K name, V obj) { V currrentValue = get(name); if (currrentValue == null || !currrentValue.equals(obj)) super.put(name, obj); } @Override public V remove(Serializable name) { V v = super.remove(name); if (v != null) { try { rpcService.executeCommandOnAllNodes(command, false, id, "r", name); } catch (Exception e) { LOG.warn("Could not remove the entry " + name + " on other cluster nodes", e); } } return v; } @Override public void clearCache() { super.clearCache(); try { rpcService.executeCommandOnAllNodes(command, false, id, "c"); } catch (Exception e) { LOG.warn("Could not clear the cache on other cluster nodes", e); } } @Override public void put(K name, V obj) { super.put(name, obj); try { rpcService.executeCommandOnAllNodes(command, false, id, "p", name, obj); } catch (Exception e) { LOG.warn("Could not put the entry " + name + " on other cluster nodes", e); } } @Override public void putMap(Map<? extends K, ? extends V> objs) { super.putMap(objs); try { rpcService.executeCommandOnAllNodes(command, false, id, "m", new HashMap<K, V>(objs)); } catch (Exception e) { LOG.warn("Could not put entries on other cluster nodes", e); } } @Override protected void finalize() throws Throwable { try { if (command != null) { rpcService.unregisterCommand(command); } } finally { super.finalize(); } } }