/* * JBoss, Home of Professional Open Source. * Copyright 2008, Red Hat Middleware LLC, and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * 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.jboss.ejb.plugins.cmp.jdbc; import org.jboss.ejb.EntityEnterpriseContext; import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCCMPFieldBridge; import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCCMRFieldBridge; import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCFieldBridge; import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCEntityBridge; import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCReadAheadMetaData; import org.jboss.logging.Logger; import org.jboss.tm.TransactionLocal; import javax.transaction.Transaction; import javax.transaction.SystemException; import java.lang.ref.SoftReference; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; /** * ReadAheadCache stores all of the data readahead for an entity. * Data is stored in the JDBCStoreManager entity tx data map on a per entity * basis. The read ahead data for each entity is stored with a soft reference. * * @author <a href="mailto:dain@daingroup.com">Dain Sundstrom</a> * @version $Revision: 81030 $ */ public final class ReadAheadCache { /** * To simplify null values handling in the preloaded data pool we use * this value instead of 'null' */ private static final Object NULL_VALUE = new Object(); private final JDBCStoreManager manager; private final Logger log; private final TransactionLocal listMapTxLocal = new TransactionLocal() { protected Object initialValue() { return new HashMap(); } public Transaction getTransaction() { try { return transactionManager.getTransaction(); } catch(SystemException e) { throw new IllegalStateException("An error occured while getting the " + "transaction associated with the current thread: " + e); } } }; private ListCache listCache; private int listCacheMax; public ReadAheadCache(JDBCStoreManager manager) { this.manager = manager; // Create the Log log = Logger.getLogger( this.getClass().getName() + "." + manager.getMetaData().getName()); } public void create() { // Create the list cache listCacheMax = ((JDBCEntityBridge)manager.getEntityBridge()).getListCacheMax(); listCache = new ListCache(listCacheMax); } public void start() { } public void stop() { listCache.clear(); } public void destroy() { listCache = null; } public void addFinderResults(List results, JDBCReadAheadMetaData readahead) { if(listCacheMax == 0 || results.size() < 2) { // nothing to see here... move along return; } Map listMap = getListMap(); if(listMap == null) { // no active transaction return; } if(log.isTraceEnabled()) { log.trace("Add finder results:" + " entity=" + manager.getEntityBridge().getEntityName() + " results=" + results + " readahead=" + readahead); } // add the finder to the LRU list if(!readahead.isNone()) { listCache.add(results); } // // Create a map between the entity primary keys and the list. // The primary key will point to the last list added that contained the // primary key. // HashSet dereferencedResults = new HashSet(); Iterator iter = results.iterator(); for(int i = 0; iter.hasNext(); i++) { Object pk = iter.next(); // create the new entry object EntityMapEntry entry; if(readahead.isNone()) { entry = new EntityMapEntry(0, Collections.singletonList(pk), readahead); } else { entry = new EntityMapEntry(i, results, readahead); } // Keep track of the results that have been dereferenced. Later we // all results from the list cache that are no longer referenced. EntityMapEntry oldInfo = (EntityMapEntry) listMap.put(pk, entry); if(oldInfo != null) { dereferencedResults.add(oldInfo.results); } } // // Now we remove all lists from the list cache that are no longer // referenced in the list map. // // if we don't have any dereferenced results at this point we are done if(dereferencedResults.isEmpty()) { return; } // // Go through the dereferenced results set and look at the PKs for each // dereferenced list. If you find one key that references the // dereferenced list, remove it from the dereferenced results set and // move on to the next dereferenced results. // iter = dereferencedResults.iterator(); while(iter.hasNext()) { List dereferencedList = (List) iter.next(); boolean listHasReference = false; Iterator iter2 = dereferencedList.iterator(); while(!listHasReference && iter2.hasNext()) { EntityMapEntry entry = (EntityMapEntry) listMap.get(iter2.next()); if(entry != null && entry.results == dereferencedList) { listHasReference = true; } } if(listHasReference) { // this list does not have any references iter.remove(); } } // if we don't have any dereferenced results at this point we are done if(dereferencedResults.isEmpty()) { return; } // remove all results from the cache that are no longer referenced iter = dereferencedResults.iterator(); while(iter.hasNext()) { List list = (List) iter.next(); if(log.isTraceEnabled()) { log.trace("Removing dereferenced results: " + list); } listCache.remove(list); } } private void removeFinderResult(List results) { Map listMap = getListMap(); if(listMap == null) { // no active transaction return; } // remove the list from the list cache listCache.remove(results); // remove all primary keys from the listMap that reference this list if(!listMap.isEmpty()) { Iterator iter = listMap.values().iterator(); while(iter.hasNext()) { EntityMapEntry entry = (EntityMapEntry) iter.next(); // use == because only identity matters here if(entry.results == results) { iter.remove(); } } } } public EntityReadAheadInfo getEntityReadAheadInfo(Object pk) { Map listMap = getListMap(); if(listMap == null) { // no active transaction return new EntityReadAheadInfo(Collections.singletonList(pk)); } EntityMapEntry entry = (EntityMapEntry) getListMap().get(pk); if(entry != null) { // we're using these results so promote it to the head of the // LRU list if(!entry.readahead.isNone()) { listCache.promote(entry.results); } // get the readahead metadata JDBCReadAheadMetaData readahead = entry.readahead; if(readahead == null) { readahead = manager.getMetaData().getReadAhead(); } int from = entry.index; int to = Math.min(entry.results.size(), entry.index + readahead.getPageSize()); List loadKeys = entry.results.subList(from, to); return new EntityReadAheadInfo(loadKeys, readahead); } else { return new EntityReadAheadInfo(Collections.singletonList(pk)); } } /** * Loads all of the preloaded data for the ctx into it. * @param ctx the context that will be loaded * @return true if at least one field was loaded. */ public boolean load(EntityEnterpriseContext ctx) { if(log.isTraceEnabled()) { log.trace("load data:" + " entity=" + manager.getEntityBridge().getEntityName() + " pk=" + ctx.getId()); } // get the preload data map Map preloadDataMap = getPreloadDataMap(ctx.getId(), false); if(preloadDataMap == null || preloadDataMap.isEmpty()) { // no preloaded data for this entity if(log.isTraceEnabled()) { log.trace("No preload data found:" + " entity=" + manager.getEntityBridge().getEntityName() + " pk=" + ctx.getId()); } return false; } boolean cleanReadAhead = manager.getMetaData().isCleanReadAheadOnLoad(); boolean loaded = false; JDBCCMRFieldBridge onlyOneSingleValuedCMR = null; // iterate over the keys in the preloaded map Iterator iter = preloadDataMap.entrySet().iterator(); while(iter.hasNext()) { Map.Entry entry = (Map.Entry) iter.next(); Object field = entry.getKey(); // get the value that was preloaded for this field Object value = entry.getValue(); // if we didn't get a value something is seriously hosed if(value == null) { throw new IllegalStateException("Preloaded value not found"); } if(cleanReadAhead) { // remove this value from the preload cache as it is about to be loaded iter.remove(); } // check for null value standin if(value == NULL_VALUE) { value = null; } if(field instanceof JDBCCMPFieldBridge) { JDBCCMPFieldBridge cmpField = (JDBCCMPFieldBridge) field; if(!cmpField.isLoaded(ctx)) { if(log.isTraceEnabled()) { log.trace("Preloading data:" + " entity=" + manager.getEntityBridge().getEntityName() + " pk=" + ctx.getId() + " cmpField=" + cmpField.getFieldName()); } // set the value cmpField.setInstanceValue(ctx, value); // mark this field clean as it's value was just loaded cmpField.setClean(ctx); loaded = true; } else { if(log.isTraceEnabled()) { log.trace("CMPField already loaded:" + " entity=" + manager.getEntityBridge().getEntityName() + " pk=" + ctx.getId() + " cmpField=" + cmpField.getFieldName()); } } } else if(field instanceof JDBCCMRFieldBridge) { JDBCCMRFieldBridge cmrField = (JDBCCMRFieldBridge) field; if(!cmrField.isLoaded(ctx)) { if(log.isTraceEnabled()) { log.trace("Preloading data:" + " entity=" + manager.getEntityBridge().getEntityName() + " pk=" + ctx.getId() + " cmrField=" + cmrField.getFieldName()); } // set the value cmrField.load(ctx, (List) value); // add the loaded list to the related entity's readahead cache JDBCStoreManager relatedManager = (JDBCStoreManager) cmrField.getRelatedCMRField().getManager(); ReadAheadCache relatedReadAheadCache = relatedManager.getReadAheadCache(); relatedReadAheadCache.addFinderResults( (List) value, cmrField.getReadAhead()); if(!loaded) { // this is a hack to fix on-load read-ahead for 1:m relationships if(cmrField.isSingleValued() && onlyOneSingleValuedCMR == null) { onlyOneSingleValuedCMR = cmrField; } else { loaded = true; } } } else { if(log.isTraceEnabled()) { log.trace("CMRField already loaded:" + " entity=" + manager.getEntityBridge().getEntityName() + " pk=" + ctx.getId() + " cmrField=" + cmrField.getFieldName()); } } } } if(cleanReadAhead) { // remove all preload data map as all of the data has been loaded manager.removeEntityTxData(new PreloadKey(ctx.getId())); } return loaded; } /** * Returns the cached value of a CMR field or null if nothing was cached for this field. * @param pk primary key. * @param cmrField the field to get the cached value for. * @return cached value for the <code>cmrField</code> or null if no value cached. */ public Collection getCachedCMRValue(Object pk, JDBCCMRFieldBridge cmrField) { Map preloadDataMap = getPreloadDataMap(pk, true); return (Collection)preloadDataMap.get(cmrField); } /** * Add preloaded data for an entity within the scope of a transaction */ public void addPreloadData(Object pk, JDBCFieldBridge field, Object fieldValue) { if(field instanceof JDBCCMRFieldBridge) { if(fieldValue == null) { fieldValue = Collections.EMPTY_LIST; } else if(!(fieldValue instanceof Collection)) { fieldValue = Collections.singletonList(fieldValue); } } if(log.isTraceEnabled()) { log.trace("Add preload data:" + " entity=" + manager.getEntityBridge().getEntityName() + " pk=" + pk + " field=" + field.getFieldName()); } // convert null values to a null value standing object if(fieldValue == null) { fieldValue = NULL_VALUE; } // store the preloaded data Map preloadDataMap = getPreloadDataMap(pk, true); Object overriden = preloadDataMap.put(field, fieldValue); if(log.isTraceEnabled() && overriden != null) { log.trace( "Overriding cached value " + overriden + " with " + (fieldValue == NULL_VALUE ? null : fieldValue) + ". pk=" + pk + ", field=" + field.getFieldName() ); } } public void removeCachedData(Object primaryKey) { if(log.isTraceEnabled()) { log.trace("Removing cached data for " + primaryKey); } Map listMap = getListMap(); if(listMap == null) { // no active tx return; } // remove the preloaded data manager.removeEntityTxData(new PreloadKey(primaryKey)); // if the entity didn't have readahead entry, or it was read-ahead // none; return EntityMapEntry oldInfo = (EntityMapEntry) listMap.remove(primaryKey); if(oldInfo == null || oldInfo.readahead.isNone()) { return; } // check to see if the dereferenced finder result is still referenced Iterator iter = listMap.values().iterator(); while(iter.hasNext()) { EntityMapEntry entry = (EntityMapEntry) iter.next(); // use == because only identity matters here if(entry.results == oldInfo.results) { // ok it is still referenced return; } } // a reference to the old finder set was not found so remove it if(log.isTraceEnabled()) { log.trace("Removing dereferenced finder results: " + oldInfo.results); } listCache.remove(oldInfo.results); } /** * Gets the map of preloaded data. * @param entityPrimaryKey the primary key of the entity * @param create should a new preload data map be created if one is not found * @return the preload data map for null if one is not found */ public Map getPreloadDataMap(Object entityPrimaryKey, boolean create) { // // Be careful in this code. A soft reference may be cleared at any time, // so don't check if a reference has a value and then get that value. // Instead get the value and then check if it is null. // // create a preload key for the entity PreloadKey preloadKey = new PreloadKey(entityPrimaryKey); // get the soft reference to the preload data map SoftReference ref = (SoftReference) manager.getEntityTxData(preloadKey); // did we get a reference if(ref != null) { // get the map from the reference Map preloadDataMap = (Map) ref.get(); // did we actually get a map? (will be null if it has been GC'd) if(preloadDataMap != null) { return preloadDataMap; } } // // at this point we did not get an existing value // // if we got a dead reference remove it if(ref != null) { //log.info(manager.getMetaData().getName() + " was GC'd from read ahead"); manager.removeEntityTxData(preloadKey); } // if we are not creating, we're done if(!create) { return null; } // create the new preload data map Map preloadDataMap = new HashMap(); // create new soft reference ref = new SoftReference(preloadDataMap); // store the reference manager.putEntityTxData(preloadKey, ref); // return the new preload data map return preloadDataMap; } private Map getListMap() { return (Map) listMapTxLocal.get(); } private final class ListCache { private final TransactionLocal cacheTxLocal = new TransactionLocal() { protected Object initialValue() { return new LinkedList(); } public Transaction getTransaction() { try { return transactionManager.getTransaction(); } catch(SystemException e) { throw new IllegalStateException("An error occured while getting the " + "transaction associated with the current thread: " + e); } } }; private int max; public ListCache(int max) { if(max < 0) throw new IllegalArgumentException("list-cache-max is negative: " + max); this.max = max; } public void add(List list) { if(max == 0) { // we're not caching lists, so we're done return; } LinkedList cache = getCache(); if(cache == null) return; cache.addFirst(new IdentityObject(list)); // shrink size to max while(cache.size() > max) { IdentityObject object = (IdentityObject) cache.removeLast(); ageOut((List) object.getObject()); } } public void promote(List list) { if(max == 0) { // we're not caching lists, so we're done return; } LinkedList cache = getCache(); if(cache == null) return; IdentityObject object = new IdentityObject(list); if(cache.remove(object)) { // it was in the cache so add it to the front cache.addFirst(object); } } public void remove(List list) { if(max == 0) { // we're not caching lists, so we're done return; } LinkedList cache = getCache(); if(cache != null) cache.remove(new IdentityObject(list)); } public void clear() { if(max == 0) { // we're not caching lists, so we're done return; } } private void ageOut(List list) { removeFinderResult(list); } private LinkedList getCache() { return (LinkedList) cacheTxLocal.get(); } } /** * Wraps an entity primary key, so it does not collide with other * data stored in the entityTxDataMap. */ private static final class PreloadKey { private final Object entityPrimaryKey; public PreloadKey(Object entityPrimaryKey) { if(entityPrimaryKey == null) { throw new IllegalArgumentException("Entity primary key is null"); } this.entityPrimaryKey = entityPrimaryKey; } public boolean equals(Object object) { if(object instanceof PreloadKey) { PreloadKey preloadKey = (PreloadKey) object; return preloadKey.entityPrimaryKey.equals(entityPrimaryKey); } return false; } public int hashCode() { return entityPrimaryKey.hashCode(); } public String toString() { return "PreloadKey: entityId=" + entityPrimaryKey; } } private static final class EntityMapEntry { public final int index; public final List results; public final JDBCReadAheadMetaData readahead; private EntityMapEntry( int index, List results, JDBCReadAheadMetaData readahead) { this.index = index; this.results = results; this.readahead = readahead; } } public final static class EntityReadAheadInfo { private final List loadKeys; private final JDBCReadAheadMetaData readahead; private EntityReadAheadInfo(List loadKeys) { this(loadKeys, null); } private EntityReadAheadInfo(List loadKeys, JDBCReadAheadMetaData r) { this.loadKeys = loadKeys; this.readahead = r; } public List getLoadKeys() { return loadKeys; } public JDBCReadAheadMetaData getReadAhead() { return readahead; } } /** * Wraps an Object and does equals/hashCode based on object identity. */ private static final class IdentityObject { private final Object object; public IdentityObject(Object object) { if(object == null) { throw new IllegalArgumentException("Object is null"); } this.object = object; } public Object getObject() { return object; } public boolean equals(Object object) { return this.object == object; } public int hashCode() { return object.hashCode(); } public String toString() { return object.toString(); } } }