001: package org.apache.ojb.broker.cache;
002:
003: /* Copyright 2004-2005 The Apache Software Foundation
004: *
005: * Licensed under the Apache License, Version 2.0 (the "License");
006: * you may not use this file except in compliance with the License.
007: * You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017:
018: import java.lang.ref.ReferenceQueue;
019: import java.lang.ref.SoftReference;
020: import java.util.ArrayList;
021: import java.util.Hashtable;
022: import java.util.Iterator;
023: import java.util.List;
024: import java.util.Map;
025: import java.util.Properties;
026:
027: import org.apache.commons.lang.builder.ToStringBuilder;
028: import org.apache.commons.lang.builder.ToStringStyle;
029: import org.apache.ojb.broker.Identity;
030: import org.apache.ojb.broker.OJBRuntimeException;
031: import org.apache.ojb.broker.PBStateEvent;
032: import org.apache.ojb.broker.PBStateListener;
033: import org.apache.ojb.broker.PersistenceBroker;
034: import org.apache.ojb.broker.util.logging.Logger;
035: import org.apache.ojb.broker.util.logging.LoggerFactory;
036:
037: /**
038: * This global ObjectCache stores all Objects loaded by the <code>PersistenceBroker</code>
039: * from a DB using a static {@link java.util.Map}. This means each {@link ObjectCache}
040: * instance associated with all {@link PersistenceBroker} instances use the same
041: * <code>Map</code> to cache objects. This could lead in "dirty-reads" (similar to read-uncommitted
042: * mode in DB) when a concurrent thread look up same object modified by another thread.
043: * <br/>
044: * When the PersistenceBroker tries to get an Object by its {@link Identity}.
045: * It first lookups the cache if the object has been already loaded and cached.
046: * <p/>
047: * NOTE: By default objects cached via {@link SoftReference} which allows
048: * objects (softly) referenced by the cache to be reclaimed by the Java Garbage Collector when
049: * they are not longer referenced elsewhere, so lifetime of cached object is limited by
050: * <br/> - the lifetime of the cache object - see property <code>timeout</code>.
051: * <br/> - the garabage collector used memory settings - see property <code>useSoftReferences</code>.
052: * <br/> - the maximum capacity of the cache - see property <code>maxEntry</code>.
053: * </p>
054: * <p/>
055: * Implementation configuration properties:
056: * </p>
057: * <p/>
058: * <p/>
059: * <table cellspacing="2" cellpadding="2" border="3" frame="box">
060: * <tr>
061: * <td><strong>Property Key</strong></td>
062: * <td><strong>Property Values</strong></td>
063: * </tr>
064: * <p/>
065: * <tr>
066: * <td>timeout</td>
067: * <td>
068: * Lifetime of the cached objects in seconds.
069: * If expired the cached object was not returned
070: * on lookup call (and removed from cache). Default timeout
071: * value is 900 seconds. When set to <tt>-1</tt> the lifetime of
072: * the cached object depends only on GC and do never get timed out.
073: * </td>
074: * </tr>
075: * <p/>
076: * <tr>
077: * <td>autoSync</td>
078: * <td>
079: * If set <tt>true</tt> all cached/looked up objects within a PB-transaction are traced.
080: * If the the PB-transaction was aborted all traced objects will be removed from
081: * cache. Default is <tt>false</tt>.
082: * <p/>
083: * NOTE: This does not prevent "dirty-reads" (more info see above).
084: * </p>
085: * <p/>
086: * It's not a smart solution for keeping cache in sync with DB but should do the job
087: * in most cases.
088: * <br/>
089: * E.g. if you lookup 1000 objects within a transaction and modify one object and then abort the
090: * transaction, 1000 objects will be passed to cache, 1000 objects will be traced and
091: * all 1000 objects will be removed from cache. If you read these objects without tx or
092: * in a former tx and then modify one object in a tx and abort the tx, only one object was
093: * traced/removed.
094: * </p>
095: * </td>
096: * </tr>
097: * <p/>
098: * <tr>
099: * <td>cachingKeyType</td>
100: * <td>
101: * Determines how the key was build for the cached objects:
102: * <br/>
103: * 0 - Identity object was used as key, this was the <em>default</em> setting.
104: * <br/>
105: * 1 - Idenity + jcdAlias name was used as key. Useful when the same object metadata model
106: * (DescriptorRepository instance) are used for different databases (JdbcConnectionDescriptor)
107: * <br/>
108: * 2 - Identity + model (DescriptorRepository) was used as key. Useful when different metadata
109: * model (DescriptorRepository instance) are used for the same database. Keep in mind that there
110: * was no synchronization between cached objects with same Identity but different metadata model.
111: * <br/>
112: * 3 - all together (1+2)
113: * </td>
114: * </tr>
115: * <p/>
116: * <tr>
117: * <td>useSoftReferences</td>
118: * <td>
119: * If set <em>true</em> this class use {@link java.lang.ref.SoftReference} to cache
120: * objects. Default value is <em>true</em>.
121: * </td>
122: * </tr>
123: * </table>
124: * <p/>
125: *
126: * @author <a href="mailto:thma@apache.org">Thomas Mahler<a>
127: * @version $Id: ObjectCacheDefaultImpl.java,v 1.24.2.7 2005/12/21 22:24:15 tomdz Exp $
128: */
129: public class ObjectCacheDefaultImpl implements ObjectCacheInternal,
130: PBStateListener {
131: private Logger log = LoggerFactory
132: .getLogger(ObjectCacheDefaultImpl.class);
133:
134: public static final String TIMEOUT_PROP = "timeout";
135: public static final String AUTOSYNC_PROP = "autoSync";
136: public static final String CACHING_KEY_TYPE_PROP = "cachingKeyType";
137: public static final String SOFT_REFERENCES_PROP = "useSoftReferences";
138: /**
139: * static Map held all cached objects
140: */
141: protected static final Map objectTable = new Hashtable();
142: private static final ReferenceQueue queue = new ReferenceQueue();
143:
144: private static long hitCount = 0;
145: private static long failCount = 0;
146: private static long gcCount = 0;
147:
148: protected PersistenceBroker broker;
149: private List identitiesInWork;
150: /**
151: * Timeout of the cached objects. Default was 900 seconds.
152: */
153: private long timeout = 1000 * 60 * 15;
154: private boolean useAutoSync = false;
155: /**
156: * Determines how the key was build for the cached objects:
157: * <br/>
158: * 0 - Identity object was used as key
159: * 1 - Idenity + jcdAlias name was used as key
160: * 2 - Identity + model (DescriptorRepository) was used as key
161: * 3 - all together (1+2)
162: */
163: private int cachingKeyType;
164: private boolean useSoftReferences = true;
165:
166: public ObjectCacheDefaultImpl(PersistenceBroker broker,
167: Properties prop) {
168: this .broker = broker;
169: timeout = prop == null ? timeout : (Long.parseLong(prop
170: .getProperty(TIMEOUT_PROP, "" + (60 * 15))) * 1000);
171: useSoftReferences = prop != null
172: && (Boolean.valueOf((prop.getProperty(
173: SOFT_REFERENCES_PROP, "true")).trim()))
174: .booleanValue();
175: cachingKeyType = prop == null ? 0 : (Integer.parseInt(prop
176: .getProperty(CACHING_KEY_TYPE_PROP, "0")));
177: useAutoSync = prop != null
178: && (Boolean.valueOf((prop.getProperty(AUTOSYNC_PROP,
179: "false")).trim())).booleanValue();
180: if (useAutoSync) {
181: if (broker != null) {
182: // we add this instance as a permanent PBStateListener
183: broker.addListener(this , true);
184: } else {
185: log.info("Can't enable property '" + AUTOSYNC_PROP
186: + "', because given PB instance is null");
187: }
188: }
189: identitiesInWork = new ArrayList();
190: if (log.isEnabledFor(Logger.INFO)) {
191: ToStringBuilder buf = new ToStringBuilder(this );
192: buf.append("timeout", timeout).append("useSoftReferences",
193: useSoftReferences).append("cachingKeyType",
194: cachingKeyType).append("useAutoSync", useAutoSync);
195: log.info("Setup cache: " + buf.toString());
196: }
197: }
198:
199: /**
200: * Clear ObjectCache. I.e. remove all entries for classes and objects.
201: */
202: public void clear() {
203: //processQueue();
204: objectTable.clear();
205: identitiesInWork.clear();
206: }
207:
208: public void doInternalCache(Identity oid, Object obj, int type) {
209: //processQueue();
210: if ((obj != null)) {
211: traceIdentity(oid);
212: synchronized (objectTable) {
213: if (log.isDebugEnabled())
214: log.debug("Cache object " + oid);
215: objectTable.put(buildKey(oid), buildEntry(obj, oid));
216: }
217: }
218: }
219:
220: /**
221: * Makes object persistent to the Objectcache.
222: * I'm using soft-references to allow gc reclaim unused objects
223: * even if they are still cached.
224: */
225: public void cache(Identity oid, Object obj) {
226: doInternalCache(oid, obj, ObjectCacheInternal.TYPE_UNKNOWN);
227: }
228:
229: public boolean cacheIfNew(Identity oid, Object obj) {
230: //processQueue();
231: boolean result = false;
232: Object key = buildKey(oid);
233: if ((obj != null)) {
234: synchronized (objectTable) {
235: if (!objectTable.containsKey(key)) {
236: objectTable.put(key, buildEntry(obj, oid));
237: result = true;
238: }
239: }
240: if (result)
241: traceIdentity(oid);
242: }
243: return result;
244: }
245:
246: /**
247: * Lookup object with Identity oid in objectTable.
248: * Returns null if no matching id is found
249: */
250: public Object lookup(Identity oid) {
251: processQueue();
252: hitCount++;
253: Object result = null;
254:
255: CacheEntry entry = (CacheEntry) objectTable.get(buildKey(oid));
256: if (entry != null) {
257: result = entry.get();
258: if (result == null
259: || entry.getLifetime() < System.currentTimeMillis()) {
260: /*
261: cached object was removed by gc or lifetime was exhausted
262: remove CacheEntry from map
263: */
264: gcCount++;
265: remove(oid);
266: // make sure that we return null
267: result = null;
268: } else {
269: /*
270: TODO: Not sure if this makes sense, could help to avoid corrupted objects
271: when changed in tx but not stored.
272: */
273: traceIdentity(oid);
274: if (log.isDebugEnabled())
275: log.debug("Object match " + oid);
276: }
277: } else {
278: failCount++;
279: }
280: return result;
281: }
282:
283: /**
284: * Removes an Object from the cache.
285: */
286: public void remove(Identity oid) {
287: //processQueue();
288: if (oid != null) {
289: removeTracedIdentity(oid);
290: objectTable.remove(buildKey(oid));
291: if (log.isDebugEnabled())
292: log.debug("Remove object " + oid);
293: }
294: }
295:
296: public String toString() {
297: ToStringBuilder buf = new ToStringBuilder(this ,
298: ToStringStyle.DEFAULT_STYLE);
299: buf.append("Count of cached objects", objectTable.keySet()
300: .size());
301: buf.append("Lookup hits", hitCount);
302: buf.append("Failures", failCount);
303: buf.append("Reclaimed", gcCount);
304: return buf.toString();
305: }
306:
307: private void traceIdentity(Identity oid) {
308: if (useAutoSync && (broker != null) && broker.isInTransaction()) {
309: identitiesInWork.add(oid);
310: }
311: }
312:
313: private void removeTracedIdentity(Identity oid) {
314: identitiesInWork.remove(oid);
315: }
316:
317: private void synchronizeWithTracedObjects() {
318: Identity oid;
319: log.info("tx was aborted," + " remove "
320: + identitiesInWork.size()
321: + " traced (potentially modified) objects from cache");
322: for (Iterator iterator = identitiesInWork.iterator(); iterator
323: .hasNext();) {
324: oid = (Identity) iterator.next();
325: objectTable.remove(buildKey(oid));
326: }
327: }
328:
329: public void beforeRollback(PBStateEvent event) {
330: synchronizeWithTracedObjects();
331: identitiesInWork.clear();
332: }
333:
334: public void beforeCommit(PBStateEvent event) {
335: // identitiesInWork.clear();
336: }
337:
338: public void beforeClose(PBStateEvent event) {
339: /*
340: arminw: In managed environments listener method "beforeClose" is called twice
341: (when the PB handle is closed and when the real PB instance is closed/returned to pool).
342: We are only interested in the real close call when all work is done.
343: */
344: if (!broker.isInTransaction()) {
345: identitiesInWork.clear();
346: }
347: }
348:
349: public void afterRollback(PBStateEvent event) {
350: }
351:
352: public void afterCommit(PBStateEvent event) {
353: identitiesInWork.clear();
354: }
355:
356: public void afterBegin(PBStateEvent event) {
357: }
358:
359: public void beforeBegin(PBStateEvent event) {
360: }
361:
362: public void afterOpen(PBStateEvent event) {
363: }
364:
365: private CacheEntry buildEntry(Object obj, Identity oid) {
366: if (useSoftReferences) {
367: return new CacheEntrySoft(obj, oid, queue, timeout);
368: } else {
369: return new CacheEntryHard(obj, oid, timeout);
370: }
371: }
372:
373: private void processQueue() {
374: CacheEntry sv;
375: while ((sv = (CacheEntry) queue.poll()) != null) {
376: removeTracedIdentity(sv.getOid());
377: objectTable.remove(buildKey(sv.getOid()));
378: }
379: }
380:
381: private Object buildKey(Identity oid) {
382: Object key;
383: switch (cachingKeyType) {
384: case 0:
385: key = oid;
386: break;
387: case 1:
388: key = new OrderedTuple(oid, broker.getPBKey().getAlias());
389: break;
390: case 2:
391: /*
392: this ObjectCache implementation only works in single JVM, so the hashCode
393: of the DescriptorRepository class is unique
394: TODO: problem when different versions of same DR are used
395: */
396: key = new OrderedTuple(oid, new Integer(broker
397: .getDescriptorRepository().hashCode()));
398: break;
399: case 3:
400: key = new OrderedTuple(oid, broker.getPBKey().getAlias(),
401: new Integer(broker.getDescriptorRepository()
402: .hashCode()));
403: break;
404: default:
405: throw new OJBRuntimeException(
406: "Unexpected error, 'cacheType =" + cachingKeyType
407: + "' was not supported");
408: }
409: return key;
410: }
411:
412: //-----------------------------------------------------------
413: // inner class to build unique key for cached objects
414: //-----------------------------------------------------------
415: /**
416: * Implements equals() and hashCode() for an ordered tuple of constant(!)
417: * objects
418: *
419: * @author Gerhard Grosse
420: * @since Oct 12, 2004
421: */
422: static final class OrderedTuple {
423: private static int[] multipliers = new int[] { 13, 17, 19, 23,
424: 29, 31, 37, 41, 43, 47, 51 };
425:
426: private Object[] elements;
427: private int hashCode;
428:
429: public OrderedTuple(Object element) {
430: elements = new Object[1];
431: elements[0] = element;
432: hashCode = calcHashCode();
433: }
434:
435: public OrderedTuple(Object element1, Object element2) {
436: elements = new Object[2];
437: elements[0] = element1;
438: elements[1] = element2;
439: hashCode = calcHashCode();
440: }
441:
442: public OrderedTuple(Object element1, Object element2,
443: Object element3) {
444: elements = new Object[3];
445: elements[0] = element1;
446: elements[1] = element2;
447: elements[2] = element3;
448: hashCode = calcHashCode();
449: }
450:
451: public OrderedTuple(Object[] elements) {
452: this .elements = elements;
453: this .hashCode = calcHashCode();
454: }
455:
456: private int calcHashCode() {
457: int code = 7;
458: for (int i = 0; i < elements.length; i++) {
459: int m = i % multipliers.length;
460: code += elements[i].hashCode() * multipliers[m];
461: }
462: return code;
463: }
464:
465: public boolean equals(Object obj) {
466: if (!(obj instanceof OrderedTuple)) {
467: return false;
468: } else {
469: OrderedTuple other = (OrderedTuple) obj;
470: if (this .hashCode != other.hashCode) {
471: return false;
472: } else if (this .elements.length != other.elements.length) {
473: return false;
474: } else {
475: for (int i = 0; i < elements.length; i++) {
476: if (!this .elements[i].equals(other.elements[i])) {
477: return false;
478: }
479: }
480: return true;
481: }
482: }
483: }
484:
485: public int hashCode() {
486: return hashCode;
487: }
488:
489: public String toString() {
490: StringBuffer s = new StringBuffer();
491: s.append('{');
492: for (int i = 0; i < elements.length; i++) {
493: s.append(elements[i]).append('#').append(
494: elements[i].hashCode()).append(',');
495: }
496: s.setCharAt(s.length() - 1, '}');
497: s.append("#").append(hashCode);
498: return s.toString();
499: }
500: }
501:
502: //-----------------------------------------------------------
503: // inner classes to wrap cached objects
504: //-----------------------------------------------------------
505: interface CacheEntry {
506: Object get();
507:
508: Identity getOid();
509:
510: long getLifetime();
511: }
512:
513: final static class CacheEntrySoft extends SoftReference implements
514: CacheEntry {
515: private final long lifetime;
516: private final Identity oid;
517:
518: CacheEntrySoft(Object object, final Identity k,
519: final ReferenceQueue q, long timeout) {
520: super (object, q);
521: oid = k;
522: // if timeout is negative, lifetime of object never expire
523: if (timeout < 0) {
524: lifetime = Long.MAX_VALUE;
525: } else {
526: lifetime = System.currentTimeMillis() + timeout;
527: }
528: }
529:
530: public Identity getOid() {
531: return oid;
532: }
533:
534: public long getLifetime() {
535: return lifetime;
536: }
537: }
538:
539: final static class CacheEntryHard implements CacheEntry {
540: private final long lifetime;
541: private final Identity oid;
542: private Object obj;
543:
544: CacheEntryHard(Object object, final Identity k, long timeout) {
545: obj = object;
546: oid = k;
547: // if timeout is negative, lifetime of object never expire
548: if (timeout < 0) {
549: lifetime = Long.MAX_VALUE;
550: } else {
551: lifetime = System.currentTimeMillis() + timeout;
552: }
553: }
554:
555: public Object get() {
556: return obj;
557: }
558:
559: public Identity getOid() {
560: return oid;
561: }
562:
563: public long getLifetime() {
564: return lifetime;
565: }
566: }
567: }
|