/* * 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.jcr.impl.dataflow.persistent; import org.exoplatform.services.jcr.config.CacheEntry; import org.exoplatform.services.jcr.config.RepositoryConfigurationException; import org.exoplatform.services.jcr.config.WorkspaceEntry; import org.exoplatform.services.jcr.dataflow.ItemState; import org.exoplatform.services.jcr.dataflow.ItemStateChangesLog; import org.exoplatform.services.jcr.dataflow.persistent.PersistedNodeData; import org.exoplatform.services.jcr.dataflow.persistent.PersistedPropertyData; import org.exoplatform.services.jcr.dataflow.persistent.WorkspaceStorageCache; import org.exoplatform.services.jcr.dataflow.persistent.WorkspaceStorageCacheListener; import org.exoplatform.services.jcr.datamodel.ItemData; import org.exoplatform.services.jcr.datamodel.ItemType; import org.exoplatform.services.jcr.datamodel.NodeData; import org.exoplatform.services.jcr.datamodel.NullItemData; import org.exoplatform.services.jcr.datamodel.PropertyData; import org.exoplatform.services.jcr.datamodel.QPath; import org.exoplatform.services.jcr.datamodel.QPathEntry; import org.exoplatform.services.jcr.impl.Constants; import org.exoplatform.services.jcr.impl.core.itemfilters.QPathEntryFilter; import org.exoplatform.services.log.ExoLogger; import org.exoplatform.services.log.Log; import org.picocontainer.Startable; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.ConcurrentModificationException; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Timer; import java.util.TimerTask; import java.util.WeakHashMap; import java.util.concurrent.Semaphore; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.ReentrantLock; /** * Created by The eXo Platform SAS.<br> * * This cache implementation store item data and childs lists of item data. And it implements * OBJECTS cache - i.e. returns same java object that was cached before. Same item data or list of * childs will be returned from getXXX() calls. * * @author <a href="mailto:peter.nedonosko@exoplatform.com.ua">Peter Nedonosko</a> * @version $Id: LinkedWorkspaceStorageCacheImpl.java 34801 2009-07-31 15:44:50Z dkatayev $ */ public class LinkedWorkspaceStorageCacheImpl implements WorkspaceStorageCache, Startable { /** * Maximum cache size. */ public static final int MAX_CACHE_SIZE = 2048; // 2 /** * Maximum cache luve time in milliseconds. */ public static final long MAX_CACHE_LIVETIME = 600 * 1000; // 10 /** * Default statistic gathering period. */ public static final long DEF_STATISTIC_PERIOD = 5 * 60000; // 5 /** * Default cache C cleaner period. */ public static final long DEF_CLEANER_PERIOD = 20 * 60000; // 20 /** * Default blocking users amount. */ public static final int DEF_BLOCKING_USERS_COUNT = 0; public static final String DEEP_DELETE_PARAMETER_NAME = "deep-delete"; /** * statistic-period parameter name. */ public static final String STATISTIC_PERIOD_PARAMETER_NAME = "statistic-period"; /** * statistic-clean parameter name. */ public static final String STATISTIC_CLEAN_PARAMETER_NAME = "statistic-clean"; /** * statistic-log parameter name. */ public static final String STATISTIC_LOG_PARAMETER_NAME = "statistic-log"; /** * blocking-users-count parameter name. */ public static final String BLOCKING_USERS_COUNT_PARAMETER_NAME = "blocking-users-count"; /** * cleaner-period parameter name. */ public static final String CLEANER_PERIOD_PARAMETER_NAME = "cleaner-period"; /** * Default cache C load factor (HashMap parameter). */ public static final float LOAD_FACTOR = 0.7f; /** * Cache implementaion logger. */ protected static final Log LOG = ExoLogger.getLogger("exo.jcr.component.core.LinkedWorkspaceStorageCacheImpl"); /** * Cache C. */ private final Map<CacheKey, CacheValue> cache; /** * Cache C lock for concurrent usage. */ private final CacheLock writeLock = new CacheLock(); /** * Cache CN (Child nodes cache). */ private final WeakHashMap<String, List<NodeData>> nodesCache; /** * Cache CP (Child properties). */ private final WeakHashMap<String, List<PropertyData>> propertiesCache; /** * Cache name. */ private final String name; /** * Enable flag. */ private boolean enabled; /** * Worker timer (statistics and cleaner). */ private final Timer workerTimer; /** * Last gathring cache statistic. */ private CacheStatistic statistic; /** * Tell if we haveto remove whole node subtree (true), or just remove cached childs lists (false). * <br> If true - it's more expensive operation. */ private final boolean deepDelete; /** * Cache C item live time. */ private long liveTime; /** * Cache C maximum size. */ private final int maxSize; /** * Statistic miss current value. */ private final AtomicLong miss = new AtomicLong(); /** * Statistic hits current value. */ private final AtomicLong hits = new AtomicLong(); /** * Statistic totalGetTime current value. */ private volatile long totalGetTime = 0; /** * cleanStatistics configuration. */ private final boolean cleanStatistics; /** * showStatistic configuration. */ private final boolean showStatistic; /** * Cache C lock class. */ class CacheLock extends ReentrantLock { Collection<Thread> getLockThreads() { return getQueuedThreads(); } Thread getLockOwner() { return getOwner(); } } /** * Cache C map impl. */ class CacheMap<K extends CacheKey, V extends CacheValue> extends LinkedHashMap<K, V> { CacheMap(long maxSize, float loadFactor) { super(Math.round(maxSize / loadFactor) + 100, loadFactor); } @Override protected boolean removeEldestEntry(final Entry<K, V> eldest) { if (size() > maxSize) { // remove with subnodes final CacheValue v = eldest.getValue(); if (v != null) { final ItemData item = v.getItem(); if (item.isNode()) { // removing childs of the node nodesCache.remove(item.getIdentifier()); propertiesCache.remove(item.getIdentifier()); } } return true; } else { return false; } } } /** * Cache C map uses blocking on get operation. */ class BlockingCacheMap<K extends CacheKey, V extends CacheValue> extends CacheMap<K, V> { private final CacheLock userLock = new CacheLock(); /** * Single user cache map. * * @param maxSize * @param loadFactor */ BlockingCacheMap(long maxSize, float loadFactor) { super(maxSize, loadFactor); } /** * {@inheritDoc} */ @Override public boolean containsValue(Object value) { userLock.lock(); try { return super.containsValue(value); } finally { userLock.unlock(); } } /** * {@inheritDoc} */ @Override public V get(Object key) { userLock.lock(); try { return super.get(key); } finally { userLock.unlock(); } } } /** * Cache C map uses blocking on get operation. */ class GroupBlockingCacheMap<K extends CacheKey, V extends CacheValue> extends CacheMap<K, V> { private final Semaphore usersLock; /** * Cache map allowes a limited set of concurrent users. * * @param maxSize * @param loadFactor * @param limit */ GroupBlockingCacheMap(long maxSize, float loadFactor, int limit) { super(maxSize, loadFactor); this.usersLock = new Semaphore(limit); } /** * {@inheritDoc} */ @Override public boolean containsValue(Object value) { try { usersLock.acquire(); } catch (InterruptedException e) { LOG.warn("Error in cache.containsValue, current thread is interrupted.", e); return false; } try { return super.containsValue(value); } finally { usersLock.release(); } } /** * {@inheritDoc} */ @Override public V get(Object key) { try { usersLock.acquire(); } catch (InterruptedException e) { LOG.warn("Error in cache.get, return null, current thread is interrupted.", e); return null; } try { return super.get(key); } finally { usersLock.release(); } } } /** * Cache worker base class. */ abstract class Worker extends Thread { protected volatile boolean done = false; Worker() { setDaemon(true); } } /** * Cache cleaner. */ class Cleaner extends Worker { private final Log log; Cleaner(Log log) { this.log = log; } /** * {@inheritDoc} */ @Override public void run() { try { cleanExpired(); } finally { done = true; } } /** * Remove expired cache C records. */ private void cleanExpired() { if (writeLock.tryLock()) { // lock writers (putItem()) String lockOwnerId = ""; try { lockOwnerId = String.valueOf(writeLock.getLockOwner()); int sizeBefore = cache.size(); int expiredCount = 0; long start = System.currentTimeMillis(); // WARN: cache map must be non modified during the operation, // writeLock care about this for (Iterator<Map.Entry<CacheKey, CacheValue>> citer = cache.entrySet().iterator(); citer.hasNext();) { Map.Entry<CacheKey, CacheValue> ce = citer.next(); if (ce.getValue().getExpiredTime() <= System.currentTimeMillis()) { ItemData item = ce.getValue().getItem(); if (item != null) { removeExpiredChilds(item); } citer.remove(); expiredCount++; } } if (log.isDebugEnabled()) { log.debug("Cleaner task done in " + (System.currentTimeMillis() - start) + "ms. Size " + sizeBefore + " -> " + cache.size() + ", " + expiredCount + " processed."); } } catch (ConcurrentModificationException e) { if (log.isDebugEnabled()) { StringBuilder lockUsers = new StringBuilder(); for (Thread user : writeLock.getLockThreads()) { lockUsers.append(user.toString()); lockUsers.append(','); } log.error("Cleaner task error, cache in use. On-write owner [" + lockOwnerId + "], users [" + lockUsers.toString() + "], error " + e, e); } // else it's not matter for work, the task will try next time } catch (Throwable e) //NOSONAR { if (log.isDebugEnabled()) { log.error("Cleaner task error " + e, e); } else { log.error("Cleaner task error " + e + ". Will try next time."); } } finally { writeLock.unlock(); } } else // skip if lock is used by another process if (log.isDebugEnabled()) { log.debug("Cleaner task skipped. Ceche in use by another process [" + String.valueOf(writeLock.getLockOwner()) + "]. Will try next time."); } } /** * Remove child items of the expired one. * * @param item */ private void removeExpiredChilds(final ItemData item) { if (item.isNode()) { // remove this node properties list if (propertiesCache.remove(item.getIdentifier()) != null) { if (log.isDebugEnabled()) { log.debug(name + ", removeExpiredChilds() propertiesCache.remove " + item.getIdentifier()); } } } else { // removing properties list of the property parent if (propertiesCache.remove(item.getParentIdentifier()) != null) { if (log.isDebugEnabled()) { log.debug(name + ", removeExpiredChilds() propertiesCache.remove " + item.getParentIdentifier()); } } } } } /** * Statistics collector. */ class StatisticCollector extends Worker { /* * (non-Javadoc) * @see java.lang.Thread#run() */ @Override public void run() { try { gatherStatistic(); } finally { done = true; } } } /** * Base worker task calss. */ abstract class WorkerTask extends TimerTask { protected Log log = ExoLogger.getLogger("exo.jcr.component.core.LinkedWorkspaceStorageCacheImpl_Worker"); protected Worker currentWorker = null; } /** * Cleaner task class. */ class CleanerTask extends WorkerTask { @Override public void run() { if (currentWorker == null || currentWorker.done) { // start worker and go out currentWorker = new Cleaner(log); currentWorker.start(); } else // skip if previous in progress if (log.isDebugEnabled()) { log.debug("Cleaner task skipped. Previous one still runs. Will try next time."); } } } /** * Gathering statistic task. */ class StatisticTask extends WorkerTask { @Override public void run() { if (currentWorker == null || currentWorker.done) { // start worker and go out currentWorker = new StatisticCollector(); currentWorker.start(); } else // skip if previous in progress if (log.isDebugEnabled()) { log.debug("Statistic task skipped. Previous one still runs. Will try next time."); } } } /** * For debug/testing. * * @param name * @param enabled * @param maxSize * @param cleanStatistics * @param blockingUsers * @param showStatistic * @throws RepositoryConfigurationException */ public LinkedWorkspaceStorageCacheImpl(String name, boolean enabled, int maxSize, long liveTimeSec, long cleanerPeriodMillis, long statisticPeriodMillis, boolean deepDelete, boolean cleanStatistics, int blockingUsers, boolean showStatistic) throws RepositoryConfigurationException { this.name = name; this.maxSize = maxSize; this.liveTime = liveTimeSec * 1000; // seconds this.nodesCache = new WeakHashMap<String, List<NodeData>>(); this.propertiesCache = new WeakHashMap<String, List<PropertyData>>(); this.enabled = enabled; this.deepDelete = deepDelete; this.cleanStatistics = cleanStatistics; this.cache = createCacheMap(blockingUsers); this.workerTimer = new Timer(this.name + "_CacheWorker", true); scheduleTask(new CleanerTask(), 5, cleanerPeriodMillis); // start after 5 sec this.showStatistic = showStatistic; gatherStatistic(); scheduleTask(new StatisticTask(), 5, statisticPeriodMillis); // start after 5 sec } /** * Create cache using container configuration entry. * * @param wsConfig * workspace configuration * @throws RepositoryConfigurationException * in case of missconfiguration */ public LinkedWorkspaceStorageCacheImpl(WorkspaceEntry wsConfig) throws RepositoryConfigurationException { this.name = "jcr." + wsConfig.getUniqueName(); CacheEntry cacheConfig = wsConfig.getCache(); long statisticPeriod; long cleanerPeriod; boolean cleanStats; int blockingUsers; boolean showStatistic; if (cacheConfig != null) { this.enabled = cacheConfig.isEnabled(); int maxSizeConf; try { maxSizeConf = cacheConfig.getParameterInteger(MAX_SIZE_PARAMETER_NAME); } catch (RepositoryConfigurationException e) { maxSizeConf = cacheConfig.getParameterInteger("maxSize"); } this.maxSize = maxSizeConf; int initialSize = maxSize > MAX_CACHE_SIZE ? maxSize / 4 : maxSize; this.nodesCache = new WeakHashMap<String, List<NodeData>>(initialSize, LOAD_FACTOR); this.propertiesCache = new WeakHashMap<String, List<PropertyData>>(initialSize, LOAD_FACTOR); try { // apply in milliseconds this.liveTime = cacheConfig.getParameterTime(LIVE_TIME_PARAMETER_NAME); } catch (RepositoryConfigurationException e) { this.liveTime = cacheConfig.getParameterTime("liveTime"); } this.deepDelete = cacheConfig.getParameterBoolean(DEEP_DELETE_PARAMETER_NAME, false); blockingUsers = cacheConfig.getParameterInteger(BLOCKING_USERS_COUNT_PARAMETER_NAME, DEF_BLOCKING_USERS_COUNT); cleanerPeriod = cacheConfig.getParameterTime(CLEANER_PERIOD_PARAMETER_NAME, DEF_CLEANER_PERIOD); cleanStats = cacheConfig.getParameterBoolean(STATISTIC_CLEAN_PARAMETER_NAME, true); statisticPeriod = cacheConfig.getParameterTime(STATISTIC_PERIOD_PARAMETER_NAME, DEF_STATISTIC_PERIOD); showStatistic = cacheConfig.getParameterBoolean(STATISTIC_LOG_PARAMETER_NAME, false); } else { this.maxSize = MAX_CACHE_SIZE; this.liveTime = MAX_CACHE_LIVETIME; this.nodesCache = new WeakHashMap<String, List<NodeData>>(); this.propertiesCache = new WeakHashMap<String, List<PropertyData>>(); this.enabled = true; this.deepDelete = false; blockingUsers = DEF_BLOCKING_USERS_COUNT; statisticPeriod = DEF_STATISTIC_PERIOD; cleanerPeriod = DEF_CLEANER_PERIOD; cleanStats = true; showStatistic = false; } this.cleanStatistics = cleanStats; this.showStatistic = showStatistic;// name if (blockingUsers > 2048) { // limit it with 2k blockingUsers = 2048; LOG.warn(BLOCKING_USERS_COUNT_PARAMETER_NAME + " maximum is limited to 2k. Using " + blockingUsers); } this.cache = createCacheMap(blockingUsers); this.workerTimer = new Timer(this.name + "_CacheWorker", true); // cleaner int start = ((int)cleanerPeriod) / 2; // half or period start = start > 60000 ? start : 60000; // don't start now scheduleTask(new CleanerTask(), start, cleanerPeriod); // statistic collector gatherStatistic(); start = ((int)statisticPeriod) / 2; // half or period start = start > 15000 ? start : 15000; // don't start now scheduleTask(new StatisticTask(), start, statisticPeriod); } private Map<CacheKey, CacheValue> createCacheMap(int blockingUsers) { if (blockingUsers <= 0) { // full access cache if (LOG.isDebugEnabled()) { LOG.debug(this.name + " Create unblocking cache map."); } return new CacheMap<CacheKey, CacheValue>(maxSize, LOAD_FACTOR); } else if (blockingUsers == 1) { // per user locked cache (get-lock) if (LOG.isDebugEnabled()) { LOG.debug(this.name + " Create per-user blocking cache map."); } return new BlockingCacheMap<CacheKey, CacheValue>(maxSize, LOAD_FACTOR); } else { // per users (count) locked cache (get-locks) if (LOG.isDebugEnabled()) { LOG.debug(this.name + " Create per-users-group blocking cache map."); } return new GroupBlockingCacheMap<CacheKey, CacheValue>(maxSize, LOAD_FACTOR, blockingUsers); } } private void scheduleTask(TimerTask task, int start, long period) { Calendar firstTime = Calendar.getInstance(); firstTime.add(Calendar.MILLISECOND, start); if (period < 30000) { // warn and use 30sec. LOG.warn("Cache worker schedule period too short " + period + ". Will use 30sec."); period = 30000; } workerTimer.schedule(task, firstTime.getTime(), period); } private void gatherStatistic() { final CacheStatistic st = new CacheStatistic(miss.get(), hits.get(), cache.size(), nodesCache.size(), propertiesCache.size(), maxSize, liveTime, totalGetTime); if (showStatistic) { try { double rel = st.getMiss() > 0 && st.getHits() > 0 ? (Math.round((10000d * st.getHits()) / st.getMiss())) / 10000d : 0; if (LOG.isDebugEnabled()) { LOG.debug("Cache " + name + ": relevancy " + rel + " (hits:" + st.getHits() + ", miss:" + st.getMiss() + "), get:" + Math.round((st.getHits() + st.getMiss()) / (st.getTotalGetTime() > 0 ? st.getTotalGetTime() / 1000d : 1)) + "oper/sec (" + (st.getTotalGetTime() / 1000d) + "sec)" + ", size:" + st.getSize() + " (max " + st.getMaxSize() + ")" + ", childs(nodes:" + st.getNodesSize() + ", properties:" + st.getPropertiesSize() + ")"); } } catch (Throwable e) //NOSONAR { LOG.warn("Show statistic log.info error " + e.getMessage()); } } this.statistic = st; if (cleanStatistics) { miss.set(0); hits.set(0); totalGetTime = 0; } } /** * {@inheritDoc} */ public long getSize() { return cache.size(); } /** * getName. * * @return */ public String getName() { return name; } /** * {@inheritDoc} */ public ItemData get(final String identifier) { if (enabled && identifier != null) { try { return getItem(identifier); } catch (Exception e) { LOG.error("GET operation fails. Item ID=" + identifier + ". Error " + e + ". NULL returned.", e); } } return null; } /** * {@inheritDoc} */ public ItemData get(final String parentId, final QPathEntry name, ItemType itemType) { if (enabled && parentId != null && name != null) { try { ItemData itemData = null; if (itemType == ItemType.NODE || itemType == ItemType.UNKNOWN) { itemData = getItem(parentId, name, ItemType.NODE); } if (itemType == ItemType.PROPERTY || itemType == ItemType.UNKNOWN && itemData == null) { itemData = getItem(parentId, name, ItemType.PROPERTY); } return itemData; } catch (Exception e) { LOG.error("GET operation fails. Parent ID=" + parentId + " name " + (name != null ? name.getAsString() : name) + ". Error " + e + ". NULL returned.", e); } } return null; } /** * Put item in cache C. * * @param data */ protected void putItem(final ItemData data) { cache.put(new CacheId(data.getIdentifier()), new CacheValue(data, System.currentTimeMillis() + liveTime)); cache.put(new CacheQPath(data.getParentIdentifier(), data.getQPath(), ItemType.getItemType(data)), new CacheValue(data, System.currentTimeMillis() + liveTime)); } /** * {@inheritDoc} */ public void put(final ItemData item) { if (enabled && item != null) { if (item instanceof NullItemData) { // skip null values return; } writeLock.lock(); try { if (LOG.isDebugEnabled()) { LOG.debug(name + ", put() " + item.getQPath().getAsString() + " " + item.getIdentifier() + " -- " + item); } putItem(item); // add child item data to list of childs of the parent if (item.isNode()) { // add child node List<NodeData> cachedParentChilds = nodesCache.get(item.getParentIdentifier()); if (cachedParentChilds != null) { // Playing for orderable work NodeData nodeData = (NodeData)item; int orderNumber = nodeData.getOrderNumber(); synchronized (cachedParentChilds) { int index = cachedParentChilds.indexOf(nodeData); if (index >= 0) { if (orderNumber != cachedParentChilds.get(index).getOrderNumber()) { // replace and reorder List<NodeData> newChilds = new ArrayList<NodeData>(cachedParentChilds.size()); for (int ci = 0; ci < cachedParentChilds.size(); ci++) { if (index == ci) { newChilds.add(nodeData); // place in new position } else { newChilds.add(cachedParentChilds.get(ci)); // copy } } nodesCache.put(item.getParentIdentifier(), newChilds); // cache // new // list if (LOG.isDebugEnabled()) { LOG.debug(name + ", put() update child node " + nodeData.getIdentifier() + " order #" + orderNumber); } } else { cachedParentChilds.set(index, nodeData); // replace at // current position if (LOG.isDebugEnabled()) { LOG.debug(name + ", put() update child node " + nodeData.getIdentifier() + " at index #" + index); } } } else { // add new to the end List<NodeData> newChilds = new ArrayList<NodeData>(cachedParentChilds.size() + 1); for (int ci = 0; ci < cachedParentChilds.size(); ci++) { newChilds.add(cachedParentChilds.get(ci)); } newChilds.add(nodeData); // add nodesCache.put(item.getParentIdentifier(), newChilds); // cache new list if (LOG.isDebugEnabled()) { LOG.debug(name + ", put() add child node " + nodeData.getIdentifier()); } } } } } else { // add child property final List<PropertyData> cachedParentChilds = propertiesCache.get(item.getParentIdentifier()); if (cachedParentChilds != null) { if (cachedParentChilds.get(0).getValues().size() > 0) { synchronized (cachedParentChilds) { // if it's a props list with values, update it int index = cachedParentChilds.indexOf(item); if (index >= 0) { // update already cached in list cachedParentChilds.set(index, (PropertyData)item); // replace at current position if (LOG.isDebugEnabled()) { LOG.debug(name + ", put() update child property " + item.getIdentifier() + " at index #" + index); } } else if (index == -1) { // add new List<PropertyData> newChilds = new ArrayList<PropertyData>(cachedParentChilds.size() + 1); for (int ci = 0; ci < cachedParentChilds.size(); ci++) { newChilds.add(cachedParentChilds.get(ci)); } newChilds.add((PropertyData)item); propertiesCache.put(item.getParentIdentifier(), newChilds); // cache new list if (LOG.isDebugEnabled()) { LOG.debug(name + ", put() add child property " + item.getIdentifier()); } } } } else { // if it's a props list with empty values, remove cached list propertiesCache.remove(item.getParentIdentifier()); } } } } catch (Exception e) { LOG.error(name + ", Error put item data in cache: " + (item != null ? item.getQPath().getAsString() : "[null]"), e); } finally { writeLock.unlock(); } } } /** * {@inheritDoc} */ public void addChildProperties(final NodeData parentData, final List<PropertyData> childItems) { if (enabled && parentData != null && childItems != null) { String logInfo = null; if (LOG.isDebugEnabled()) { logInfo = "parent: " + parentData.getQPath().getAsString() + " " + parentData.getIdentifier() + " " + childItems.size(); LOG.debug(name + ", addChildProperties() >>> " + logInfo); } String operName = ""; // for debug/trace only writeLock.lock(); try { // remove parent (no childs) operName = "removing parent"; removeItem(parentData); operName = "caching parent"; putItem(parentData); // put parent in cache synchronized (childItems) { operName = "caching child properties list"; propertiesCache.put(parentData.getIdentifier(), childItems); // put childs in cache CP operName = "caching child properties"; // put childs in cache C for (ItemData p : childItems) { if (LOG.isDebugEnabled()) { LOG.debug(name + ", addChildProperties() " + p.getQPath().getAsString() + " " + p.getIdentifier() + " -- " + p); } putItem(p); } } } catch (Exception e) { LOG.error(name + ", Error in addChildProperties() " + operName + ": parent " + (parentData != null ? parentData.getQPath().getAsString() : "[null]"), e); } finally { writeLock.unlock(); } if (LOG.isDebugEnabled()) { LOG.debug(name + ", addChildProperties() <<< " + logInfo); } } } /** * {@inheritDoc} */ public void addChildPropertiesList(final NodeData parentData, final List<PropertyData> childItems) { if (enabled && parentData != null && childItems != null) { String logInfo = null; if (LOG.isDebugEnabled()) { logInfo = "parent: " + parentData.getQPath().getAsString() + " " + parentData.getIdentifier() + " " + childItems.size(); LOG.debug(name + ", addChildPropertiesList() >>> " + logInfo); } String operName = ""; // for debug/trace only writeLock.lock(); try { // remove parent (no childs) operName = "removing parent"; removeItem(parentData); operName = "caching parent"; putItem(parentData); // put parent in cache synchronized (childItems) { operName = "caching child properties list"; propertiesCache.put(parentData.getIdentifier(), childItems); // put childs in cache CP } } catch (Exception e) { LOG.error(name + ", Error in addChildPropertiesList() " + operName + ": parent " + (parentData != null ? parentData.getQPath().getAsString() : "[null]"), e); } finally { writeLock.unlock(); } if (LOG.isDebugEnabled()) { LOG.debug(name + ", addChildPropertiesList() <<< " + logInfo); } } } /** * {@inheritDoc} */ public void addChildNodes(final NodeData parentData, final List<NodeData> childItems) { if (enabled && parentData != null && childItems != null) { String logInfo = null; if (LOG.isDebugEnabled()) { logInfo = "parent: " + parentData.getQPath().getAsString() + " " + parentData.getIdentifier() + " " + childItems.size(); LOG.debug(name + ", addChildNodes() >>> " + logInfo); } String operName = ""; // for debug/trace only writeLock.lock(); try { // remove parent (no childs) operName = "removing parent"; removeItem(parentData); operName = "caching parent"; putItem(parentData); // put parent in cache synchronized (childItems) { operName = "caching child nodes list"; nodesCache.put(parentData.getIdentifier(), childItems); // put childs // in cache CN operName = "caching child nodes"; // put childs in cache C for (ItemData n : childItems) { if (LOG.isDebugEnabled()) { LOG.debug(name + ", addChildNodes() " + n.getQPath().getAsString() + " " + n.getIdentifier() + " -- " + n); } putItem(n); } } } catch (Exception e) { LOG.error(name + ", Error in addChildNodes() " + operName + ": parent " + (parentData != null ? parentData.getQPath().getAsString() : "[null]"), e); } finally { writeLock.unlock(); } if (LOG.isDebugEnabled()) { LOG.debug(name + ", addChildNodes() <<< " + logInfo); } } } /** * Removes data and its children in cache.<br> * Implementation details<br> * Remove Item from cache C, for Node removes lists in CN and CP (only lists).<br> * Remove Item from parent's child lists (CN for Node, CP for Property). NOTE: if CN or CP of the * Item parent are iterrating now ConcurrentModificationException will occurs there. NOTE #2: used * from onSaveItems(). * * @param item */ public void remove(final ItemData item) { if (enabled && item != null) { if (LOG.isDebugEnabled()) { LOG.debug(name + ", remove() " + item.getQPath().getAsString() + " " + item.getIdentifier()); } writeLock.lock(); try { final String itemId = item.getIdentifier(); removeItem(item); if (item.isNode()) { // removing childs of the node nodesCache.remove(itemId); propertiesCache.remove(itemId); // removing child from the node's parent child nodes list removeChildNode(item.getParentIdentifier(), itemId); } else { removeChildProperty(item.getParentIdentifier(), itemId); } } catch (Exception e) { LOG.error(name + ", Error remove item data from cache: " + item.getQPath().getAsString(), e); } finally { writeLock.unlock(); } } } /** * {@inheritDoc} */ public void remove(String identifier, ItemData item) { writeLock.lock(); try { ItemData data= getItem(identifier); if (data != null && data.equals(item)) { cache.remove(new CacheId(identifier)); } } catch (Exception e) { LOG.error(name + ", Error remove item data from cache: " + item.getQPath().getAsString(), e); } finally { writeLock.unlock(); } } /** * Get item from cache C by item id. Checks is it expired, calcs statistics. * * @param identifier * @return item */ protected ItemData getItem(final String identifier) { long start = System.currentTimeMillis(); try { final CacheId k = new CacheId(identifier); final CacheValue v = cache.get(k); if (v != null) { final ItemData c = v.getItem(); if (v.getExpiredTime() > System.currentTimeMillis()) { // check if wasn't removed if (LOG.isDebugEnabled()) { LOG.debug(name + ", getItem() " + identifier + " --> " + (c != null ? c.getQPath().getAsString() + " parent:" + c.getParentIdentifier() : "[null]")); } hits.incrementAndGet(); return c; } // remove expired writeLock.lock(); try { cache.remove(k); // remove by parentId + path cache.remove(new CacheQPath(c.getParentIdentifier(), c.getQPath(), ItemType.getItemType(c))); // remove cached child lists if (c.isNode()) { nodesCache.remove(c.getIdentifier()); propertiesCache.remove(c.getIdentifier()); } } finally { writeLock.unlock(); } } miss.incrementAndGet(); return null; } finally { totalGetTime += System.currentTimeMillis() - start; } } /** * Get item from cache C by item parent and name. Checks is it expired, calcs statistics. * * @param parentUuid * @param qname * @param itemType */ protected ItemData getItem(final String parentUuid, final QPathEntry qname, ItemType itemType) { long start = System.currentTimeMillis(); try { final CacheQPath k = new CacheQPath(parentUuid, qname, itemType); final CacheValue v = cache.get(k); if (v != null) { final ItemData c = v.getItem(); if (v.getExpiredTime() > System.currentTimeMillis()) { if (LOG.isDebugEnabled()) { LOG.debug(name + ", getItem() " + (c != null ? c.getQPath().getAsString() : "[null]") + " --> " + (c != null ? c.getIdentifier() + " parent:" + c.getParentIdentifier() : "[null]")); } hits.incrementAndGet(); return c; } // remove expired writeLock.lock(); try { cache.remove(k); // remove by Id cache.remove(new CacheId(c.getIdentifier())); // remove cached child lists if (c.isNode()) { nodesCache.remove(c.getIdentifier()); propertiesCache.remove(c.getIdentifier()); } } finally { writeLock.unlock(); } } miss.incrementAndGet(); return null; } finally { totalGetTime += System.currentTimeMillis() - start; } } /** * {@inheritDoc} */ public List<NodeData> getChildNodes(final NodeData parentData) { if (enabled && parentData != null) { long start = System.currentTimeMillis(); try { // we assume that parent cached too final List<NodeData> cn = nodesCache.get(parentData.getIdentifier()); if (LOG.isDebugEnabled()) { LOG.debug(name + ", getChildNodes() " + parentData.getQPath().getAsString() + " " + parentData.getIdentifier()); final StringBuilder blog = new StringBuilder(); if (cn != null) { blog.append("\n"); for (NodeData nd : cn) { blog.append("\t\t" + nd.getQPath().getAsString() + " " + nd.getIdentifier() + "\n"); } LOG.debug("\t-->" + blog.toString()); } else { LOG.debug("\t--> null"); } } if (cn != null) { hits.incrementAndGet(); } else { miss.incrementAndGet(); } return cn; } catch (Exception e) { LOG.error(name + ", Error in getChildNodes() parentData: " + (parentData != null ? parentData.getQPath().getAsString() : "[null]"), e); } finally { totalGetTime += System.currentTimeMillis() - start; } } return null; // nothing cached } /** * {@inheritDoc} */ public int getChildNodesCount(NodeData parentData) { if (enabled && parentData != null) { long start = System.currentTimeMillis(); try { // we assume that parent cached too final List<NodeData> cn = nodesCache.get(parentData.getIdentifier()); if (LOG.isDebugEnabled()) { LOG.debug(name + ", getChildNodesCount() " + parentData.getQPath().getAsString() + " " + parentData.getIdentifier()); final StringBuilder blog = new StringBuilder(); if (cn != null) { blog.append("\n"); for (NodeData nd : cn) { blog.append("\t\t" + nd.getQPath().getAsString() + " " + nd.getIdentifier() + "\n"); } LOG.debug("\t-->" + blog.toString()); } else { LOG.debug("\t--> null"); } } if (cn != null) { hits.incrementAndGet(); } else { miss.incrementAndGet(); } return cn != null ? cn.size() : -1; } catch (Exception e) { LOG.error(name + ", Error in getChildNodesCount() parentData: " + (parentData != null ? parentData.getQPath().getAsString() : "[null]"), e); } finally { totalGetTime += System.currentTimeMillis() - start; } } return -1; // nothing cached } /** * {@inheritDoc} */ public List<PropertyData> getChildProperties(final NodeData parentData) { if (enabled && parentData != null) { long start = System.currentTimeMillis(); try { // we assume that parent cached too final List<PropertyData> cp = propertiesCache.get(parentData.getIdentifier()); if (LOG.isDebugEnabled()) { LOG.debug(name + ", getChildProperties() " + parentData.getQPath().getAsString() + " " + parentData.getIdentifier()); final StringBuilder blog = new StringBuilder(); if (cp != null) { blog.append("\n"); for (PropertyData pd : cp) { blog.append("\t\t" + pd.getQPath().getAsString() + " " + pd.getIdentifier() + "\n"); } LOG.debug("\t--> " + blog.toString()); } else { LOG.debug("\t--> null"); } } if (cp != null && cp.get(0).getValues().size() > 0) { // don't return list of empty-valued props (but listChildProperties() can) hits.incrementAndGet(); return cp; } else { miss.incrementAndGet(); } } catch (Exception e) { LOG.error(name + ", Error in getChildProperties() parentData: " + (parentData != null ? parentData.getQPath().getAsString() : "[null]"), e); } finally { totalGetTime += System.currentTimeMillis() - start; } } return null; // nothing cached } /** * {@inheritDoc} */ public List<PropertyData> getReferencedProperties(String identifier) { return null; } /** * {@inheritDoc} */ public void addReferencedProperties(String identifier, List<PropertyData> refProperties) { } /** * {@inheritDoc} */ public List<PropertyData> listChildProperties(final NodeData parentData) { if (enabled && parentData != null) { try { // we assume that parent cached too final List<PropertyData> cp = propertiesCache.get(parentData.getIdentifier()); if (LOG.isDebugEnabled()) { LOG.debug(name + ", listChildProperties() " + parentData.getQPath().getAsString() + " " + parentData.getIdentifier()); final StringBuilder blog = new StringBuilder(); if (cp != null) { blog.append("\n"); for (PropertyData pd : cp) { blog.append("\t\t" + pd.getQPath().getAsString() + " " + pd.getIdentifier() + "\n"); } LOG.debug("\t--> " + blog.toString()); } else { LOG.debug("\t--> null"); } } if (cp != null) { hits.incrementAndGet(); } else { miss.incrementAndGet(); } return cp; } catch (Exception e) { LOG.error(name + ", Error in listChildProperties() parentData: " + (parentData != null ? parentData.getQPath().getAsString() : "[null]"), e); } } return null; // nothing cached } /** * {@inheritDoc} */ public boolean isEnabled() { return enabled; } /** * {@inheritDoc} */ public boolean isPatternSupported() { return false; } /** * {@inheritDoc} */ public boolean isChildNodesByPageSupported() { return false; } /** * Enable cache. * * @param enabled */ public void setEnabled(boolean enabled) { this.enabled = enabled; } /** * Not supported now. * * @param maxSize */ public void setMaxSize(int maxSize) { LOG.warn("setMaxSize not supported now"); } /** * Set liveTime of newly cached items. * * @param liveTime */ public void setLiveTime(long liveTime) { writeLock.lock(); try { this.liveTime = liveTime; } finally { writeLock.unlock(); } LOG .info(name + " : set liveTime=" + liveTime + "ms. New value will be applied to items cached from this moment."); } /** * Remove item from cache C. * * @param item */ protected void removeItem(final ItemData item) { final String itemId = item.getIdentifier(); cache.remove(new CacheId(itemId)); final CacheValue v2 = cache.remove(new CacheQPath(item.getParentIdentifier(), item.getQPath(), ItemType.getItemType(item))); if (v2 != null && !v2.getItem().getIdentifier().equals(itemId)) { // same path but diff identifier node... phantom removeItem(v2.getItem()); } } /** * Remove sibling's subtrees from cache C, CN, CP.<br> For update (order-before) usecase.<br> * The work does remove of all descendants of the item parent. I.e. the node and its siblings (for * SNS case).<br> */ protected void removeSiblings(final NodeData node) { if (node.getIdentifier().equals(Constants.ROOT_UUID)) { return; } // remove child nodes of the item parent recursive writeLock.lock(); try { // remove on-parent child nodes list nodesCache.remove(node.getParentIdentifier()); // go through the C and remove every descendant of the node parent final QPath path = node.getQPath().makeParentPath(); final List<CacheId> toRemove = new ArrayList<CacheId>(); // find and remove by path for (Iterator<Map.Entry<CacheKey, CacheValue>> citer = cache.entrySet().iterator(); citer.hasNext();) { Map.Entry<CacheKey, CacheValue> ce = citer.next(); CacheKey key = ce.getKey(); CacheValue v = ce.getValue(); if (v != null) { if (key.isDescendantOf(path)) { // will remove by id too toRemove.add(new CacheId(v.getItem().getIdentifier())); citer.remove(); // remove nodesCache.remove(v.getItem().getIdentifier()); propertiesCache.remove(v.getItem().getIdentifier()); } } else { citer.remove(); // remove empty C record } } for (CacheId id : toRemove) { cache.remove(id); } toRemove.clear(); } finally { writeLock.unlock(); } } // --------------------- ItemsPersistenceListener -------------- /** * {@inheritDoc} */ public void onSaveItems(final ItemStateChangesLog changesLog) { if (!enabled) { return; } ItemState prevState = null; for (Iterator<ItemState> iter = changesLog.getAllStates().iterator(); iter.hasNext();) { ItemState state = iter.next(); ItemData item = state.getData(); if (LOG.isDebugEnabled()) { LOG.debug(name + ", onSaveItems() " + ItemState.nameFromValue(state.getState()) + " " + item.getQPath().getAsString() + " " + item.getIdentifier() + " parent:" + item.getParentIdentifier()); } try { if (state.isAdded()) { put(item); } else if (state.isDeleted()) { remove(item); } else if (state.isRenamed()) { // MOVE operation (DESTENATION changes, same as ADDED), states for whole subtree! // RENAME goes before DELETE put(item); } else if (state.isPathChanged()) { writeLock.lock(); try { for (Entry<CacheKey, CacheValue> cacheEntry : cache.entrySet()) { CacheKey cacheKey = cacheEntry.getKey(); CacheValue cacheValue = cacheEntry.getValue(); ItemData oldItemData = cacheValue.getItem(); if (oldItemData.getQPath().isDescendantOf(state.getOldPath())) { int relativeDegree = oldItemData.getQPath().getDepth() - state.getOldPath().getDepth(); QPath newQPath = QPath.makeChildPath(state.getData().getQPath(), oldItemData.getQPath().getRelPath(relativeDegree)); if (oldItemData.isNode()) { NodeData nodeData = (NodeData)oldItemData; PersistedNodeData newItemData = new PersistedNodeData(nodeData.getIdentifier(), newQPath, nodeData.getParentIdentifier(), nodeData.getPersistedVersion(), nodeData.getOrderNumber(), nodeData.getPrimaryTypeName(), nodeData.getMixinTypeNames(), nodeData.getACL()); cache.put(cacheKey, new CacheValue(newItemData, cacheValue.getExpiredTime())); // update in children nodes List<NodeData> cachedChildNodes = nodesCache.get(nodeData.getParentIdentifier()); if (cachedChildNodes != null) { int index = cachedChildNodes.indexOf(oldItemData); cachedChildNodes.set(index, newItemData); } } else { PersistedPropertyData oldPropertyData = (PersistedPropertyData)oldItemData; PersistedPropertyData newPropertyData = new PersistedPropertyData(oldPropertyData.getIdentifier(), newQPath, oldPropertyData.getParentIdentifier(), oldPropertyData.getPersistedVersion(), oldPropertyData.getType(), oldPropertyData.isMultiValued(), oldPropertyData.getValues(), new SimplePersistedSize( oldPropertyData.getPersistedSize())); cache.put(cacheKey, new CacheValue(newPropertyData, cacheValue.getExpiredTime())); // update in children properties List<PropertyData> cachedChildProps = propertiesCache.get(oldPropertyData.getParentIdentifier()); if (cachedChildProps != null) { int index = cachedChildProps.indexOf(oldItemData); cachedChildProps.set(index, newPropertyData); } } } } } finally { writeLock.unlock(); } } else if (state.isUpdated()) { // UPDATE occurs on reordered (no subtree!) and merged nodes (for each // merged-updated) if (item.isNode()) { if (prevState != null) { // play only for reorder, UPDATE goes after DELETE of same path // item // we have to unload node and its parent child nodes to be loaded // back from the persistence if (prevState.isDeleted() && prevState.getData().getParentIdentifier().equals(item.getParentIdentifier())) { removeSiblings((NodeData)item); } } } else if (item.getQPath().getName().equals(Constants.EXO_PERMISSIONS)) { // get parent Node // check if parent is mix:privilegeable ItemData parent = get(item.getParentIdentifier()); // delete parent remove(parent); writeLock.lock(); try { // delete parent containing child nodes list nodesCache.remove(parent.getParentIdentifier()); // traverse itemCache Iterator<CacheValue> cacheIterator = cache.values().iterator(); while (cacheIterator.hasNext()) { ItemData cachedItem = cacheIterator.next().getItem(); if (cachedItem.isNode()) { if (cachedItem.getQPath().isDescendantOf(parent.getQPath())) { cacheIterator.remove(); } } } // traverse child node Cache Iterator<List<NodeData>> childNodesIterator = nodesCache.values().iterator(); while (childNodesIterator.hasNext()) { List<NodeData> list = childNodesIterator.next(); if (list != null && list.size() > 0) { if (list.get(0).getQPath().isDescendantOf(parent.getQPath())) { childNodesIterator.remove(); } } } } finally { writeLock.unlock(); } } put(item); } else if (state.isMixinChanged()) { // MIXIN_CHANGED, on Node put(item); } } catch (Exception e) { LOG.error(name + ", Error process onSaveItems action for item data: " + (item != null ? item.getQPath().getAsString() : "[null]"), e); } prevState = state; } } // --------------------------------------------------- /** * Remove property by id if parent properties are cached in CP. * * @param parentIdentifier * - parent id * @param childIdentifier * - property id * @return removed property or null if property not cached or parent properties are not cached */ protected PropertyData removeChildProperty(final String parentIdentifier, final String childIdentifier) { final List<PropertyData> childProperties = propertiesCache.get(parentIdentifier); if (childProperties != null) { synchronized (childProperties) { // [PN] 17.01.07 for (Iterator<PropertyData> i = childProperties.iterator(); i.hasNext();) { PropertyData cn = i.next(); if (cn.getIdentifier().equals(childIdentifier)) { i.remove(); if (childProperties.size() <= 0) { propertiesCache.remove(parentIdentifier); } return cn; } } } } return null; } /** * Remove child node by id if parent child nodes are cached in CN. * * @param parentIdentifier * - parebt if * @param childIdentifier * - node id * @return removed node or null if node not cached or parent child nodes are not cached */ protected NodeData removeChildNode(final String parentIdentifier, final String childIdentifier) { final List<NodeData> childNodes = nodesCache.get(parentIdentifier); if (childNodes != null) { synchronized (childNodes) { // [PN] 17.01.07 for (Iterator<NodeData> i = childNodes.iterator(); i.hasNext();) { NodeData cn = i.next(); if (cn.getIdentifier().equals(childIdentifier)) { i.remove(); return cn; } } } } return null; } /** * Return last gathered statistic.<br> * * @return CacheStatistic */ public CacheStatistic getStatistic() { return statistic; } // -- for debug /** * For debug. */ String dump() { StringBuilder res = new StringBuilder(); for (Map.Entry<CacheKey, CacheValue> ce : cache.entrySet()) { res.append(ce.getKey().hashCode()); res.append("\t\t"); res.append(ce.getValue().getItem().getIdentifier()); res.append(", "); res.append(ce.getValue().getItem().getQPath().getAsString()); res.append(", "); res.append(ce.getValue().getExpiredTime()); res.append(", "); res.append(ce.getKey().getClass().getSimpleName()); res.append("\r\n"); } return res.toString(); } public void beginTransaction() { } public void commitTransaction() { } public void rollbackTransaction() { } /** * {@inheritDoc} */ public boolean isTXAware() { return true; } /** * {@inheritDoc} */ public void start() { } /** * {@inheritDoc} */ public void stop() { if (workerTimer != null) { try { workerTimer.cancel(); } catch (Throwable e) //NOSONAR { LOG.warn(this.name + " cache, stop error " + e.getMessage()); } } nodesCache.clear(); propertiesCache.clear(); cache.clear(); } public void addChildProperties(NodeData parent, QPathEntryFilter pattern, List<PropertyData> childProperties) { } public List<PropertyData> getChildProperties(NodeData parent, QPathEntryFilter pattern) { return null; } public void addChildNodes(NodeData parent, QPathEntryFilter pattern, List<NodeData> childNodes) { } public List<NodeData> getChildNodes(NodeData parent, QPathEntryFilter pattern) { return null; } public List<NodeData> getChildNodesByPage(NodeData parent, int fromOrderNum) { return null; } public void addChildNodesByPage(NodeData parent, List<NodeData> childs, int fromOrderNum) { } /** * {@inheritDoc} */ public void addListener(WorkspaceStorageCacheListener listener) throws UnsupportedOperationException { throw new UnsupportedOperationException("The cache listeners are not supported by the LinkedWorkspaceStorageCacheImpl"); } /** * {@inheritDoc} */ public void removeListener(WorkspaceStorageCacheListener listener) throws UnsupportedOperationException { throw new UnsupportedOperationException("The cache listeners are not supported by the LinkedWorkspaceStorageCacheImpl"); } /** * {@inheritDoc} */ public void addChildNodesCount(NodeData parent, int count) { } }