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 }