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.1 2007-08-24 22:17:29 ewestfal Exp $
128 */
129 public class ObjectCacheDefaultImpl implements ObjectCacheInternal, PBStateListener
130 {
131 private Logger log = LoggerFactory.getLogger(ObjectCacheDefaultImpl.class);
132
133 public static final String TIMEOUT_PROP = "timeout";
134 public static final String AUTOSYNC_PROP = "autoSync";
135 public static final String CACHING_KEY_TYPE_PROP = "cachingKeyType";
136 public static final String SOFT_REFERENCES_PROP = "useSoftReferences";
137 /**
138 * static Map held all cached objects
139 */
140 protected static final Map objectTable = new Hashtable();
141 private static final ReferenceQueue queue = new ReferenceQueue();
142
143 private static long hitCount = 0;
144 private static long failCount = 0;
145 private static long gcCount = 0;
146
147 protected PersistenceBroker broker;
148 private List identitiesInWork;
149 /**
150 * Timeout of the cached objects. Default was 900 seconds.
151 */
152 private long timeout = 1000 * 60 * 15;
153 private boolean useAutoSync = false;
154 /**
155 * Determines how the key was build for the cached objects:
156 * <br/>
157 * 0 - Identity object was used as key
158 * 1 - Idenity + jcdAlias name was used as key
159 * 2 - Identity + model (DescriptorRepository) was used as key
160 * 3 - all together (1+2)
161 */
162 private int cachingKeyType;
163 private boolean useSoftReferences = true;
164
165 public ObjectCacheDefaultImpl(PersistenceBroker broker, Properties prop)
166 {
167 this.broker = broker;
168 timeout = prop == null ? timeout : (Long.parseLong(prop.getProperty(TIMEOUT_PROP, "" + (60 * 15))) * 1000);
169 useSoftReferences = prop != null && (Boolean.valueOf((prop.getProperty(SOFT_REFERENCES_PROP, "true")).trim())).booleanValue();
170 cachingKeyType = prop == null ? 0 : (Integer.parseInt(prop.getProperty(CACHING_KEY_TYPE_PROP, "0")));
171 useAutoSync = prop != null && (Boolean.valueOf((prop.getProperty(AUTOSYNC_PROP, "false")).trim())).booleanValue();
172 if(useAutoSync)
173 {
174 if(broker != null)
175 {
176 // we add this instance as a permanent PBStateListener
177 broker.addListener(this, true);
178 }
179 else
180 {
181 log.info("Can't enable property '" + AUTOSYNC_PROP + "', because given PB instance is null");
182 }
183 }
184 identitiesInWork = new ArrayList();
185 if(log.isEnabledFor(Logger.INFO))
186 {
187 ToStringBuilder buf = new ToStringBuilder(this);
188 buf.append("timeout", timeout)
189 .append("useSoftReferences", useSoftReferences)
190 .append("cachingKeyType", cachingKeyType)
191 .append("useAutoSync", useAutoSync);
192 log.info("Setup cache: " + buf.toString());
193 }
194 }
195
196 /**
197 * Clear ObjectCache. I.e. remove all entries for classes and objects.
198 */
199 public void clear()
200 {
201 //processQueue();
202 objectTable.clear();
203 identitiesInWork.clear();
204 }
205
206 public void doInternalCache(Identity oid, Object obj, int type)
207 {
208 //processQueue();
209 if((obj != null))
210 {
211 traceIdentity(oid);
212 synchronized(objectTable)
213 {
214 if(log.isDebugEnabled()) 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 {
227 doInternalCache(oid, obj, ObjectCacheInternal.TYPE_UNKNOWN);
228 }
229
230 public boolean cacheIfNew(Identity oid, Object obj)
231 {
232 //processQueue();
233 boolean result = false;
234 Object key = buildKey(oid);
235 if((obj != null))
236 {
237 synchronized(objectTable)
238 {
239 if(!objectTable.containsKey(key))
240 {
241 objectTable.put(key, buildEntry(obj, oid));
242 result = true;
243 }
244 }
245 if(result) traceIdentity(oid);
246 }
247 return result;
248 }
249
250 /**
251 * Lookup object with Identity oid in objectTable.
252 * Returns null if no matching id is found
253 */
254 public Object lookup(Identity oid)
255 {
256 processQueue();
257 hitCount++;
258 Object result = null;
259
260 CacheEntry entry = (CacheEntry) objectTable.get(buildKey(oid));
261 if(entry != null)
262 {
263 result = entry.get();
264 if(result == null || entry.getLifetime() < System.currentTimeMillis())
265 {
266 /*
267 cached object was removed by gc or lifetime was exhausted
268 remove CacheEntry from map
269 */
270 gcCount++;
271 remove(oid);
272 // make sure that we return null
273 result = null;
274 }
275 else
276 {
277 /*
278 TODO: Not sure if this makes sense, could help to avoid corrupted objects
279 when changed in tx but not stored.
280 */
281 traceIdentity(oid);
282 if(log.isDebugEnabled()) log.debug("Object match " + oid);
283 }
284 }
285 else
286 {
287 failCount++;
288 }
289 return result;
290 }
291
292 /**
293 * Removes an Object from the cache.
294 */
295 public void remove(Identity oid)
296 {
297 //processQueue();
298 if(oid != null)
299 {
300 removeTracedIdentity(oid);
301 objectTable.remove(buildKey(oid));
302 if(log.isDebugEnabled()) log.debug("Remove object " + oid);
303 }
304 }
305
306 public String toString()
307 {
308 ToStringBuilder buf = new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE);
309 buf.append("Count of cached objects", objectTable.keySet().size());
310 buf.append("Lookup hits", hitCount);
311 buf.append("Failures", failCount);
312 buf.append("Reclaimed", gcCount);
313 return buf.toString();
314 }
315
316 private void traceIdentity(Identity oid)
317 {
318 if(useAutoSync && (broker != null) && broker.isInTransaction())
319 {
320 identitiesInWork.add(oid);
321 }
322 }
323
324 private void removeTracedIdentity(Identity oid)
325 {
326 identitiesInWork.remove(oid);
327 }
328
329 private void synchronizeWithTracedObjects()
330 {
331 Identity oid;
332 log.info("tx was aborted," +
333 " remove " + identitiesInWork.size() + " traced (potentially modified) objects from cache");
334 for(Iterator iterator = identitiesInWork.iterator(); iterator.hasNext();)
335 {
336 oid = (Identity) iterator.next();
337 objectTable.remove(buildKey(oid));
338 }
339 }
340
341 public void beforeRollback(PBStateEvent event)
342 {
343 synchronizeWithTracedObjects();
344 identitiesInWork.clear();
345 }
346
347 public void beforeCommit(PBStateEvent event)
348 {
349 // identitiesInWork.clear();
350 }
351
352 public void beforeClose(PBStateEvent event)
353 {
354 /*
355 arminw: In managed environments listener method "beforeClose" is called twice
356 (when the PB handle is closed and when the real PB instance is closed/returned to pool).
357 We are only interested in the real close call when all work is done.
358 */
359 if(!broker.isInTransaction())
360 {
361 identitiesInWork.clear();
362 }
363 }
364
365 public void afterRollback(PBStateEvent event)
366 {
367 }
368
369 public void afterCommit(PBStateEvent event)
370 {
371 identitiesInWork.clear();
372 }
373
374 public void afterBegin(PBStateEvent event)
375 {
376 }
377
378 public void beforeBegin(PBStateEvent event)
379 {
380 }
381
382 public void afterOpen(PBStateEvent event)
383 {
384 }
385
386 private CacheEntry buildEntry(Object obj, Identity oid)
387 {
388 if(useSoftReferences)
389 {
390 return new CacheEntrySoft(obj, oid, queue, timeout);
391 }
392 else
393 {
394 return new CacheEntryHard(obj, oid, timeout);
395 }
396 }
397
398 private void processQueue()
399 {
400 CacheEntry sv;
401 while((sv = (CacheEntry) queue.poll()) != null)
402 {
403 removeTracedIdentity(sv.getOid());
404 objectTable.remove(buildKey(sv.getOid()));
405 }
406 }
407
408 private Object buildKey(Identity oid)
409 {
410 Object key;
411 switch(cachingKeyType)
412 {
413 case 0:
414 key = oid;
415 break;
416 case 1:
417 key = new OrderedTuple(oid, broker.getPBKey().getAlias());
418 break;
419 case 2:
420 /*
421 this ObjectCache implementation only works in single JVM, so the hashCode
422 of the DescriptorRepository class is unique
423 TODO: problem when different versions of same DR are used
424 */
425 key = new OrderedTuple(oid,
426 new Integer(broker.getDescriptorRepository().hashCode()));
427 break;
428 case 3:
429 key = new OrderedTuple(oid, broker.getPBKey().getAlias(),
430 new Integer(broker.getDescriptorRepository().hashCode()));
431 break;
432 default:
433 throw new OJBRuntimeException("Unexpected error, 'cacheType =" + cachingKeyType + "' was not supported");
434 }
435 return key;
436 }
437
438
439 //-----------------------------------------------------------
440 // inner class to build unique key for cached objects
441 //-----------------------------------------------------------
442 /**
443 * Implements equals() and hashCode() for an ordered tuple of constant(!)
444 * objects
445 *
446 * @author Gerhard Grosse
447 * @since Oct 12, 2004
448 */
449 static final class OrderedTuple
450 {
451 private static int[] multipliers =
452 new int[]{13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 51};
453
454 private Object[] elements;
455 private int hashCode;
456
457 public OrderedTuple(Object element)
458 {
459 elements = new Object[1];
460 elements[0] = element;
461 hashCode = calcHashCode();
462 }
463
464 public OrderedTuple(Object element1, Object element2)
465 {
466 elements = new Object[2];
467 elements[0] = element1;
468 elements[1] = element2;
469 hashCode = calcHashCode();
470 }
471
472 public OrderedTuple(Object element1, Object element2, Object element3)
473 {
474 elements = new Object[3];
475 elements[0] = element1;
476 elements[1] = element2;
477 elements[2] = element3;
478 hashCode = calcHashCode();
479 }
480
481 public OrderedTuple(Object[] elements)
482 {
483 this.elements = elements;
484 this.hashCode = calcHashCode();
485 }
486
487 private int calcHashCode()
488 {
489 int code = 7;
490 for(int i = 0; i < elements.length; i++)
491 {
492 int m = i % multipliers.length;
493 code += elements[i].hashCode() * multipliers[m];
494 }
495 return code;
496 }
497
498 public boolean equals(Object obj)
499 {
500 if(!(obj instanceof OrderedTuple))
501 {
502 return false;
503 }
504 else
505 {
506 OrderedTuple other = (OrderedTuple) obj;
507 if(this.hashCode != other.hashCode)
508 {
509 return false;
510 }
511 else if(this.elements.length != other.elements.length)
512 {
513 return false;
514 }
515 else
516 {
517 for(int i = 0; i < elements.length; i++)
518 {
519 if(!this.elements[i].equals(other.elements[i]))
520 {
521 return false;
522 }
523 }
524 return true;
525 }
526 }
527 }
528
529 public int hashCode()
530 {
531 return hashCode;
532 }
533
534 public String toString()
535 {
536 StringBuffer s = new StringBuffer();
537 s.append('{');
538 for(int i = 0; i < elements.length; i++)
539 {
540 s.append(elements[i]).append('#').append(elements[i].hashCode()).append(',');
541 }
542 s.setCharAt(s.length() - 1, '}');
543 s.append("#").append(hashCode);
544 return s.toString();
545 }
546 }
547
548 //-----------------------------------------------------------
549 // inner classes to wrap cached objects
550 //-----------------------------------------------------------
551 interface CacheEntry
552 {
553 Object get();
554 Identity getOid();
555 long getLifetime();
556 }
557
558 final static class CacheEntrySoft extends SoftReference implements CacheEntry
559 {
560 private final long lifetime;
561 private final Identity oid;
562
563 CacheEntrySoft(Object object, final Identity k, final ReferenceQueue q, long timeout)
564 {
565 super(object, q);
566 oid = k;
567 // if timeout is negative, lifetime of object never expire
568 if(timeout < 0)
569 {
570 lifetime = Long.MAX_VALUE;
571 }
572 else
573 {
574 lifetime = System.currentTimeMillis() + timeout;
575 }
576 }
577
578 public Identity getOid()
579 {
580 return oid;
581 }
582
583 public long getLifetime()
584 {
585 return lifetime;
586 }
587 }
588
589 final static class CacheEntryHard implements CacheEntry
590 {
591 private final long lifetime;
592 private final Identity oid;
593 private Object obj;
594
595 CacheEntryHard(Object object, final Identity k, long timeout)
596 {
597 obj = object;
598 oid = k;
599 // if timeout is negative, lifetime of object never expire
600 if(timeout < 0)
601 {
602 lifetime = Long.MAX_VALUE;
603 }
604 else
605 {
606 lifetime = System.currentTimeMillis() + timeout;
607 }
608 }
609
610 public Object get()
611 {
612 return obj;
613 }
614
615 public Identity getOid()
616 {
617 return oid;
618 }
619
620 public long getLifetime()
621 {
622 return lifetime;
623 }
624 }
625 }