package org.apache.ojb.broker.cache; /* Copyright 2004 The Apache Software Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */
import java.lang.ref.ReferenceQueue; import java.lang.ref.SoftReference; import java.util.ArrayList; import java.util.Hashtable; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; import org.apache.commons.lang.builder.ToStringBuilder; import org.apache.commons.lang.builder.ToStringStyle; import org.apache.ojb.broker.Identity; import org.apache.ojb.broker.PBStateEvent; import org.apache.ojb.broker.PBStateListener; import org.apache.ojb.broker.PersistenceBroker; import org.apache.ojb.broker.cache.ObjectCache; import org.apache.ojb.broker.util.logging.LoggerFactory; /** * This global ObjectCache stores all Objects loaded by the <code>PersistenceBroker</code> * from a DB using a static [EMAIL PROTECTED] java.util.Map}. This means each [EMAIL PROTECTED] ObjectCache} * instance associated with all [EMAIL PROTECTED] PersistenceBroker} instances use the same * <code>Map</code> to cache objects. This could lead in "dirty-reads" (similar to read-uncommitted * mode in DB) when a concurrent thread look up same object modified by another thread. * <br/> * When the PersistenceBroker tries to get an Object by its [EMAIL PROTECTED] Identity}. * It first lookups the cache if the object has been already loaded and cached. * <br/> * The cache uses soft-references which allows objects (softly) referenced by * the cache to be reclaimed by the Java Garbage Collector when they are not * longer referenced elsewhere. * * <p> * Implementation configuration properties: * </p> * * <table cellspacing="2" cellpadding="2" border="3" frame="box"> * <tr> * <td><strong>Property Key</strong></td> * <td><strong>Property Values</strong></td> * </tr> * <tr> * <td>timeout</td> * <td> * Lifetime of the cached objects in seconds. * If expired the cached object was not returned * on lookup call (and removed from cache). Default timeout * value is 900 seconds. When set to <tt>-1</tt> the lifetime of * the cached object depends only on GC and do never get timed out. * <p> * NOTE: Objects internal cached via [EMAIL PROTECTED] SoftReference}, so * lifetime of cached object is limited by the GC too. * </p> * </td> * </tr> * <tr> * <td>autoSync</td> * <td> * If set <tt>true</tt> all cached/looked up objects within a PB-transaction are traced. * If the the PB-transaction was aborted all traced objects will be removed from * cache. Default is <tt>false</tt>. * <p> * NOTE: This does not prevent "dirty-reads" (more info see above). * </p> * <p> * It's not a smart solution for keeping cache in sync with DB but should do the job * in most cases. * <br/> * E.g. if you lookup 1000 objects within a transaction and modify one object and then abort the * transaction, 1000 objects will be passed to cache, 1000 objects will be traced and * all 1000 objects will be removed from cache. If you read these objects without tx or * in a former tx and then modify one object in a tx and abort the tx, only one object was * traced/removed. * </p> * </td> * </tr> * </table> * * <br/> * * @author <a href="mailto:[EMAIL PROTECTED]">Thomas Mahler<a> * @version $Id: ObjectCacheDefaultImpl.java,v 1.21 2004/03/11 18:16:09 brianm Exp $ */ public class ObjectCacheDefaultImpl implements ObjectCache, PBStateListener { public static final String TIMEOUT_PROP = "timeout"; public static final String AUTOSYNC_PROP = "autoSync"; /** * static Map held all cached objects */ protected static Map objectTable = new Hashtable(); private static ReferenceQueue queue = new ReferenceQueue(); private static long hitCount = 0; private static long failCount = 0; private static long gcCount = 0; protected PersistenceBroker broker; private List identitiesInWork; private boolean useAutoSync; /** * Timeout of the cached objects. Default was 900 seconds. */ private long timeout = 1000 * 60 * 15; /** * */ public ObjectCacheDefaultImpl(PersistenceBroker broker, Properties prop) { this.broker = broker; timeout = prop == null ? timeout : (Long.parseLong(prop.getProperty(TIMEOUT_PROP, "" + (60 * 15))) * 1000); useAutoSync = prop == null ? false : (Boolean.valueOf((prop.getProperty(AUTOSYNC_PROP, "false")).trim())).booleanValue(); if (useAutoSync && broker != null) { // we add this instance as a permanent PBStateListener broker.addListener(this, true); } if (useAutoSync && broker == null) { LoggerFactory.getDefaultLogger().info("[" + ObjectCacheDefaultImpl.class.getName() + "] Can't enable " + AUTOSYNC_PROP + ", because given PB instance is null"); } identitiesInWork = new ArrayList(); } /** * Clear ObjectCache. I.e. remove all entries for classes and objects. */ public void clear() { processQueue(); objectTable.clear(); identitiesInWork.clear(); } /** * Makes object persistent to the Objectcache. * I'm using soft-references to allow gc reclaim unused objects * even if they are still cached. */ public void cache(Identity oid, Object obj) { processQueue(); if ((obj != null)) { traceIdentity(oid); objectTable.put(oid, new CacheEntry(obj, oid, queue)); } } /** * Lookup object with Identity oid in objectTable. * Returns null if no matching id is found */ public Object lookup(Identity oid) { processQueue(); hitCount++; Object result = null; CacheEntry entry = (CacheEntry) objectTable.get(oid); if (entry != null) { result = entry.get(); if (result == null || entry.lifetime < System.currentTimeMillis()) { /* cached object was removed by gc or lifetime was exhausted remove CacheEntry from map */ gcCount++; remove(oid); // make sure that we return null result = null; } else { /* TODO: Not sure if this makes sense, could help to avoid corrupted objects when changed in tx but not stored. */ traceIdentity(oid); } } else { failCount++; } return result; } /** * Removes an Object from the cache. */ public void remove(Identity oid) { processQueue(); if (oid != null) { removeTracedIdentity(oid); objectTable.remove(oid); } } public String toString() { ToStringBuilder buf = new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE); buf.append("Count of cached objects", objectTable.keySet().size()); buf.append("Lookup hits", hitCount); buf.append("Failures", failCount); buf.append("Reclaimed", gcCount); return buf.toString(); } protected void finalize() { LoggerFactory.getDefaultLogger().debug("Finalize ObjectCache: " + this.toString()); } private void traceIdentity(Identity oid) { if (useAutoSync && (broker != null) && broker.isInTransaction()) { identitiesInWork.add(oid); } } private void removeTracedIdentity(Identity oid) { identitiesInWork.remove(oid); } private void synchronizeWithTracedObjects() { Identity oid; LoggerFactory.getDefaultLogger().info("[" + this.getClass().getName() + "] tx was aborted," + " remove " + identitiesInWork.size() + " traced (potentially modified) objects from cache"); for (Iterator iterator = identitiesInWork.iterator(); iterator.hasNext();) { oid = (Identity) iterator.next(); objectTable.remove(oid); } } public void beforeRollback(PBStateEvent event) { synchronizeWithTracedObjects(); } public void beforeCommit(PBStateEvent event) { identitiesInWork.clear(); } public void beforeClose(PBStateEvent event) { identitiesInWork.clear(); } public void afterRollback(PBStateEvent event) { } public void afterCommit(PBStateEvent event) { } public void afterBegin(PBStateEvent event) { } public void beforeBegin(PBStateEvent event) { } public void afterOpen(PBStateEvent event) { } private void processQueue() { CacheEntry sv; while ((sv = (CacheEntry) queue.poll()) != null) { removeTracedIdentity(sv.oid); objectTable.remove(sv.oid); // we can access private data! } } //----------------------------------------------------------- // inner class //----------------------------------------------------------- class CacheEntry extends SoftReference { private final long lifetime; private final Identity oid; public CacheEntry(Object object, final Identity k, final ReferenceQueue q) { super(object, q); oid = k; // if timeout is negative, lifetime of object never expire if (timeout < 0) { lifetime = Long.MAX_VALUE; } else { lifetime = System.currentTimeMillis() + timeout; } } } } --------------------------------------------------------------------- To unsubscribe, e-mail: [EMAIL PROTECTED] For additional commands, e-mail: [EMAIL PROTECTED]
