View Javadoc

1   package org.apache.ojb.broker.cache;
2   
3   /* Copyright 2004-2005 The Apache Software Foundation
4    *
5    * Licensed under the Apache License, Version 2.0 (the "License");
6    * you may not use this file except in compliance with the License.
7    * You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  import java.lang.ref.ReferenceQueue;
19  import java.lang.ref.SoftReference;
20  import java.util.ArrayList;
21  import java.util.Hashtable;
22  import java.util.Iterator;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.Properties;
26  
27  import org.apache.commons.lang.builder.ToStringBuilder;
28  import org.apache.commons.lang.builder.ToStringStyle;
29  import org.apache.ojb.broker.Identity;
30  import org.apache.ojb.broker.OJBRuntimeException;
31  import org.apache.ojb.broker.PBStateEvent;
32  import org.apache.ojb.broker.PBStateListener;
33  import org.apache.ojb.broker.PersistenceBroker;
34  import org.apache.ojb.broker.util.logging.Logger;
35  import org.apache.ojb.broker.util.logging.LoggerFactory;
36  
37  /**
38   * This global ObjectCache stores all Objects loaded by the <code>PersistenceBroker</code>
39   * from a DB using a static {@link java.util.Map}. This means each {@link ObjectCache}
40   * instance associated with all {@link PersistenceBroker} instances use the same
41   * <code>Map</code> to cache objects. This could lead in "dirty-reads" (similar to read-uncommitted
42   * mode in DB) when a concurrent thread look up same object modified by another thread.
43   * <br/>
44   * When the PersistenceBroker tries to get an Object by its {@link Identity}.
45   * It first lookups the cache if the object has been already loaded and cached.
46   * <p/>
47   * NOTE: By default objects cached via {@link SoftReference} which allows
48   * objects (softly) referenced by the cache to be reclaimed by the Java Garbage Collector when
49   * they are not longer referenced elsewhere, so lifetime of cached object is limited by
50   * <br/> - the lifetime of the cache object - see property <code>timeout</code>.
51   * <br/> - the garabage collector used memory settings - see property <code>useSoftReferences</code>.
52   * <br/> - the maximum capacity of the cache - see property <code>maxEntry</code>.
53   * </p>
54   * <p/>
55   * Implementation configuration properties:
56   * </p>
57   * <p/>
58   * <p/>
59   * <table cellspacing="2" cellpadding="2" border="3" frame="box">
60   * <tr>
61   * <td><strong>Property Key</strong></td>
62   * <td><strong>Property Values</strong></td>
63   * </tr>
64   * <p/>
65   * <tr>
66   * <td>timeout</td>
67   * <td>
68   * Lifetime of the cached objects in seconds.
69   * If expired the cached object was not returned
70   * on lookup call (and removed from cache). Default timeout
71   * value is 900 seconds. When set to <tt>-1</tt> the lifetime of
72   * the cached object depends only on GC and do never get timed out.
73   * </td>
74   * </tr>
75   * <p/>
76   * <tr>
77   * <td>autoSync</td>
78   * <td>
79   * If set <tt>true</tt> all cached/looked up objects within a PB-transaction are traced.
80   * If the the PB-transaction was aborted all traced objects will be removed from
81   * cache. Default is <tt>false</tt>.
82   * <p/>
83   * NOTE: This does not prevent "dirty-reads" (more info see above).
84   * </p>
85   * <p/>
86   * It's not a smart solution for keeping cache in sync with DB but should do the job
87   * in most cases.
88   * <br/>
89   * E.g. if you lookup 1000 objects within a transaction and modify one object and then abort the
90   * transaction, 1000 objects will be passed to cache, 1000 objects will be traced and
91   * all 1000 objects will be removed from cache. If you read these objects without tx or
92   * in a former tx and then modify one object in a tx and abort the tx, only one object was
93   * traced/removed.
94   * </p>
95   * </td>
96   * </tr>
97   * <p/>
98   * <tr>
99   * <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 }