Clover coverage report -
Coverage timestamp: Sun Mar 5 2006 06:05:21 CST
file stats: LOC: 964   Methods: 42
NCLOC: 414   Classes: 1
 
 Source file Conditionals Statements Methods TOTAL
Cache.java 74.2% 81.3% 78.6% 78.7%
coverage coverage
 1    /*
 2    * Copyright (c) 2002-2003 by OpenSymphony
 3    * All rights reserved.
 4    */
 5    package com.opensymphony.oscache.base;
 6   
 7    import com.opensymphony.oscache.base.algorithm.AbstractConcurrentReadCache;
 8    import com.opensymphony.oscache.base.algorithm.LRUCache;
 9    import com.opensymphony.oscache.base.algorithm.UnlimitedCache;
 10    import com.opensymphony.oscache.base.events.*;
 11    import com.opensymphony.oscache.base.persistence.PersistenceListener;
 12    import com.opensymphony.oscache.util.FastCronParser;
 13   
 14    import org.apache.commons.logging.Log;
 15    import org.apache.commons.logging.LogFactory;
 16   
 17    import java.io.Serializable;
 18   
 19    import java.text.ParseException;
 20   
 21    import java.util.*;
 22   
 23    import javax.swing.event.EventListenerList;
 24   
 25    /**
 26    * Provides an interface to the cache itself. Creating an instance of this class
 27    * will create a cache that behaves according to its construction parameters.
 28    * The public API provides methods to manage objects in the cache and configure
 29    * any cache event listeners.
 30    *
 31    * @version $Revision: 1.3 $
 32    * @author <a href="mailto:mike@atlassian.com">Mike Cannon-Brookes</a>
 33    * @author <a href="mailto:tgochenour@peregrine.com">Todd Gochenour</a>
 34    * @author <a href="mailto:fbeauregard@pyxis-tech.com">Francois Beauregard</a>
 35    * @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
 36    */
 37    public class Cache implements Serializable {
 38    /**
 39    * An event that origininated from within another event.
 40    */
 41    public static final String NESTED_EVENT = "NESTED";
 42    private static transient final Log log = LogFactory.getLog(Cache.class);
 43   
 44    /**
 45    * A list of all registered event listeners for this cache.
 46    */
 47    protected EventListenerList listenerList = new EventListenerList();
 48   
 49    /**
 50    * The actual cache map. This is where the cached objects are held.
 51    */
 52    private AbstractConcurrentReadCache cacheMap = null;
 53   
 54    /**
 55    * Date of last complete cache flush.
 56    */
 57    private Date flushDateTime = null;
 58   
 59    /**
 60    * A map that holds keys of cache entries that are currently being built, and EntryUpdateState instance as values. This is used to coordinate threads
 61    * that modify/access a same key in concurrence.
 62    *
 63    * The cache checks against this map when a stale entry is requested, or a cache miss is observed.
 64    *
 65    * If the requested key is in here, we know the entry is currently being
 66    * built by another thread and hence we can either block and wait or serve
 67    * the stale entry (depending on whether cache blocking is enabled or not).
 68    * <p>
 69    * To avoid data races, values in this map should remain present during the whole time distinct threads deal with the
 70    * same key. We implement this using explicit reference counting in the EntryUpdateState instance, to be able to clean up
 71    * the map once all threads have declared they are done accessing/updating a given key.
 72    *
 73    * It is not possible to locate this into the CacheEntry because this would require to have a CacheEntry instance for all cache misses, and
 74    * may therefore generate a memory leak. More over, the CacheEntry instance may not be hold in memory in the case no
 75    * memory cache is configured.
 76    */
 77    private Map updateStates = new HashMap();
 78   
 79    /**
 80    * Indicates whether the cache blocks requests until new content has
 81    * been generated or just serves stale content instead.
 82    */
 83    private boolean blocking = false;
 84   
 85    /**
 86    * Create a new Cache
 87    *
 88    * @param useMemoryCaching Specify if the memory caching is going to be used
 89    * @param unlimitedDiskCache Specify if the disk caching is unlimited
 90    * @param overflowPersistence Specify if the persistent cache is used in overflow only mode
 91    */
 92  12 public Cache(boolean useMemoryCaching, boolean unlimitedDiskCache, boolean overflowPersistence) {
 93  12 this(useMemoryCaching, unlimitedDiskCache, overflowPersistence, false, null, 0);
 94    }
 95   
 96    /**
 97    * Create a new Cache.
 98    *
 99    * If a valid algorithm class is specified, it will be used for this cache.
 100    * Otherwise if a capacity is specified, it will use LRUCache.
 101    * If no algorithm or capacity is specified UnlimitedCache is used.
 102    *
 103    * @see com.opensymphony.oscache.base.algorithm.LRUCache
 104    * @see com.opensymphony.oscache.base.algorithm.UnlimitedCache
 105    * @param useMemoryCaching Specify if the memory caching is going to be used
 106    * @param unlimitedDiskCache Specify if the disk caching is unlimited
 107    * @param overflowPersistence Specify if the persistent cache is used in overflow only mode
 108    * @param blocking This parameter takes effect when a cache entry has
 109    * just expired and several simultaneous requests try to retrieve it. While
 110    * one request is rebuilding the content, the other requests will either
 111    * block and wait for the new content (<code>blocking == true</code>) or
 112    * instead receive a copy of the stale content so they don't have to wait
 113    * (<code>blocking == false</code>). the default is <code>false</code>,
 114    * which provides better performance but at the expense of slightly stale
 115    * data being served.
 116    * @param algorithmClass The class implementing the desired algorithm
 117    * @param capacity The capacity
 118    */
 119  108 public Cache(boolean useMemoryCaching, boolean unlimitedDiskCache, boolean overflowPersistence, boolean blocking, String algorithmClass, int capacity) {
 120    // Instantiate the algo class if valid
 121  108 if (((algorithmClass != null) && (algorithmClass.length() > 0)) && (capacity > 0)) {
 122  16 try {
 123  16 cacheMap = (AbstractConcurrentReadCache) Class.forName(algorithmClass).newInstance();
 124  16 cacheMap.setMaxEntries(capacity);
 125    } catch (Exception e) {
 126  0 log.error("Invalid class name for cache algorithm class. " + e.toString());
 127    }
 128    }
 129   
 130  108 if (cacheMap == null) {
 131    // If we have a capacity, use LRU cache otherwise use unlimited Cache
 132  92 if (capacity > 0) {
 133  36 cacheMap = new LRUCache(capacity);
 134    } else {
 135  56 cacheMap = new UnlimitedCache();
 136    }
 137    }
 138   
 139  108 cacheMap.setUnlimitedDiskCache(unlimitedDiskCache);
 140  108 cacheMap.setOverflowPersistence(overflowPersistence);
 141  108 cacheMap.setMemoryCaching(useMemoryCaching);
 142   
 143  108 this.blocking = blocking;
 144    }
 145   
 146    /**
 147    * Allows the capacity of the cache to be altered dynamically. Note that
 148    * some cache implementations may choose to ignore this setting (eg the
 149    * {@link UnlimitedCache} ignores this call).
 150    *
 151    * @param capacity the maximum number of items to hold in the cache.
 152    */
 153  16 public void setCapacity(int capacity) {
 154  16 cacheMap.setMaxEntries(capacity);
 155    }
 156   
 157    /**
 158    * Checks if the cache was flushed more recently than the CacheEntry provided.
 159    * Used to determine whether to refresh the particular CacheEntry.
 160    *
 161    * @param cacheEntry The cache entry which we're seeing whether to refresh
 162    * @return Whether or not the cache has been flushed more recently than this cache entry was updated.
 163    */
 164  221 public boolean isFlushed(CacheEntry cacheEntry) {
 165  221 if (flushDateTime != null) {
 166  0 long lastUpdate = cacheEntry.getLastUpdate();
 167   
 168  0 return (flushDateTime.getTime() >= lastUpdate);
 169    } else {
 170  221 return false;
 171    }
 172    }
 173   
 174    /**
 175    * Retrieve an object from the cache specifying its key.
 176    *
 177    * @param key Key of the object in the cache.
 178    *
 179    * @return The object from cache
 180    *
 181    * @throws NeedsRefreshException Thrown when the object either
 182    * doesn't exist, or exists but is stale. When this exception occurs,
 183    * the CacheEntry corresponding to the supplied key will be locked
 184    * and other threads requesting this entry will potentially be blocked
 185    * until the caller repopulates the cache. If the caller choses not
 186    * to repopulate the cache, they <em>must</em> instead call
 187    * {@link #cancelUpdate(String)}.
 188    */
 189  8000 public Object getFromCache(String key) throws NeedsRefreshException {
 190  8000 return getFromCache(key, CacheEntry.INDEFINITE_EXPIRY, null);
 191    }
 192   
 193    /**
 194    * Retrieve an object from the cache specifying its key.
 195    *
 196    * @param key Key of the object in the cache.
 197    * @param refreshPeriod How long before the object needs refresh. To
 198    * allow the object to stay in the cache indefinitely, supply a value
 199    * of {@link CacheEntry#INDEFINITE_EXPIRY}.
 200    *
 201    * @return The object from cache
 202    *
 203    * @throws NeedsRefreshException Thrown when the object either
 204    * doesn't exist, or exists but is stale. When this exception occurs,
 205    * the CacheEntry corresponding to the supplied key will be locked
 206    * and other threads requesting this entry will potentially be blocked
 207    * until the caller repopulates the cache. If the caller choses not
 208    * to repopulate the cache, they <em>must</em> instead call
 209    * {@link #cancelUpdate(String)}.
 210    */
 211  2004396 public Object getFromCache(String key, int refreshPeriod) throws NeedsRefreshException {
 212  2004396 return getFromCache(key, refreshPeriod, null);
 213    }
 214   
 215    /**
 216    * Retrieve an object from the cache specifying its key.
 217    *
 218    * @param key Key of the object in the cache.
 219    * @param refreshPeriod How long before the object needs refresh. To
 220    * allow the object to stay in the cache indefinitely, supply a value
 221    * of {@link CacheEntry#INDEFINITE_EXPIRY}.
 222    * @param cronExpiry A cron expression that specifies fixed date(s)
 223    * and/or time(s) that this cache entry should
 224    * expire on.
 225    *
 226    * @return The object from cache
 227    *
 228    * @throws NeedsRefreshException Thrown when the object either
 229    * doesn't exist, or exists but is stale. When this exception occurs,
 230    * the CacheEntry corresponding to the supplied key will be locked
 231    * and other threads requesting this entry will potentially be blocked
 232    * until the caller repopulates the cache. If the caller choses not
 233    * to repopulate the cache, they <em>must</em> instead call
 234    * {@link #cancelUpdate(String)}.
 235    */
 236  2012396 public Object getFromCache(String key, int refreshPeriod, String cronExpiry) throws NeedsRefreshException {
 237  2012396 CacheEntry cacheEntry = this.getCacheEntry(key, null, null);
 238   
 239  2012381 Object content = cacheEntry.getContent();
 240  2012382 CacheMapAccessEventType accessEventType = CacheMapAccessEventType.HIT;
 241   
 242  2012381 boolean reload = false;
 243   
 244    // Check if this entry has expired or has not yet been added to the cache. If
 245    // so, we need to decide whether to block, serve stale content or throw a
 246    // NeedsRefreshException
 247  2012382 if (this.isStale(cacheEntry, refreshPeriod, cronExpiry)) {
 248   
 249    //Get access to the EntryUpdateState instance and increment the usage count during the potential sleep
 250  2012159 EntryUpdateState updateState = getUpdateState(key);
 251  2012163 try {
 252  2012163 synchronized (updateState) {
 253  2012163 if (updateState.isAwaitingUpdate() || updateState.isCancelled()) {
 254    // No one else is currently updating this entry - grab ownership
 255  1974663 updateState.startUpdate();
 256   
 257  1974663 if (cacheEntry.isNew()) {
 258  8026 accessEventType = CacheMapAccessEventType.MISS;
 259    } else {
 260  1966637 accessEventType = CacheMapAccessEventType.STALE_HIT;
 261    }
 262  37500 } else if (updateState.isUpdating()) {
 263    // Another thread is already updating the cache. We block if this
 264    // is a new entry, or blocking mode is enabled. Either putInCache()
 265    // or cancelUpdate() can cause this thread to resume.
 266  37500 if (cacheEntry.isNew() || blocking) {
 267  37496 do {
 268  1581251 try {
 269  1581251 updateState.wait();
 270    } catch (InterruptedException e) {
 271    }
 272  1581251 } while (updateState.isUpdating());
 273   
 274  37496 if (updateState.isCancelled()) {
 275    // The updating thread cancelled the update, let this one have a go.
 276    // This increments the usage count for this EntryUpdateState instance
 277  37479 updateState.startUpdate();
 278   
 279  37479 if (cacheEntry.isNew()) {
 280  4 accessEventType = CacheMapAccessEventType.MISS;
 281    } else {
 282  37475 accessEventType = CacheMapAccessEventType.STALE_HIT;
 283    }
 284  17 } else if (updateState.isComplete()) {
 285  17 reload = true;
 286    } else {
 287  0 log.error("Invalid update state for cache entry " + key);
 288    }
 289    }
 290    } else {
 291  0 reload = true;
 292    }
 293    }
 294    } finally {
 295    //Make sure we release the usage count for this EntryUpdateState since we don't use it anymore. If the current thread started the update, then the counter was
 296    //increased by one in startUpdate()
 297  2012163 releaseUpdateState(updateState, key);
 298    }
 299    }
 300   
 301    // If reload is true then another thread must have successfully rebuilt the cache entry
 302  2012384 if (reload) {
 303  17 cacheEntry = (CacheEntry) cacheMap.get(key);
 304   
 305  17 if (cacheEntry != null) {
 306  17 content = cacheEntry.getContent();
 307    } else {
 308  0 log.error("Could not reload cache entry after waiting for it to be rebuilt");
 309    }
 310    }
 311   
 312  2012384 dispatchCacheMapAccessEvent(accessEventType, cacheEntry, null);
 313   
 314    // If we didn't end up getting a hit then we need to throw a NRE
 315  2012384 if (accessEventType != CacheMapAccessEventType.HIT) {
 316  2012142 throw new NeedsRefreshException(content);
 317    }
 318   
 319  242 return content;
 320    }
 321   
 322    /**
 323    * Set the listener to use for data persistence. Only one
 324    * <code>PersistenceListener</code> can be configured per cache.
 325    *
 326    * @param listener The implementation of a persistance listener
 327    */
 328  54 public void setPersistenceListener(PersistenceListener listener) {
 329  54 cacheMap.setPersistenceListener(listener);
 330    }
 331   
 332    /**
 333    * Retrieves the currently configured <code>PersistenceListener</code>.
 334    *
 335    * @return the cache's <code>PersistenceListener</code>, or <code>null</code>
 336    * if no listener is configured.
 337    */
 338  0 public PersistenceListener getPersistenceListener() {
 339  0 return cacheMap.getPersistenceListener();
 340    }
 341   
 342    /**
 343    * Register a listener for Cache events. The listener must implement
 344    * one of the child interfaces of the {@link CacheEventListener} interface.
 345    *
 346    * @param listener The object that listens to events.
 347    */
 348  96 public void addCacheEventListener(CacheEventListener listener, Class clazz) {
 349  96 if (CacheEventListener.class.isAssignableFrom(clazz)) {
 350  96 listenerList.add(clazz, listener);
 351    } else {
 352  0 log.error("The class '" + clazz.getName() + "' is not a CacheEventListener. Ignoring this listener.");
 353    }
 354    }
 355   
 356    /**
 357    * Returns the list of all CacheEventListeners.
 358    * @return the CacheEventListener's list of the Cache
 359    */
 360  0 public EventListenerList getCacheEventListenerList() {
 361  0 return listenerList;
 362    }
 363   
 364    /**
 365    * Cancels any pending update for this cache entry. This should <em>only</em>
 366    * be called by the thread that is responsible for performing the update ie
 367    * the thread that received the original {@link NeedsRefreshException}.<p/>
 368    * If a cache entry is not updated (via {@link #putInCache} and this method is
 369    * not called to let OSCache know the update will not be forthcoming, subsequent
 370    * requests for this cache entry will either block indefinitely (if this is a new
 371    * cache entry or cache.blocking=true), or forever get served stale content. Note
 372    * however that there is no harm in cancelling an update on a key that either
 373    * does not exist or is not currently being updated.
 374    *
 375    * @param key The key for the cache entry in question.
 376    */
 377  2008121 public void cancelUpdate(String key) {
 378  2008121 EntryUpdateState state;
 379   
 380  2008121 if (key != null) {
 381  2008121 synchronized (updateStates) {
 382  2008121 state = (EntryUpdateState) updateStates.get(key);
 383   
 384  2008121 if (state != null) {
 385  2008121 synchronized (state) {
 386  2008121 int usageCounter = state.cancelUpdate();
 387  2008121 state.notify();
 388   
 389  2008121 checkEntryStateUpdateUsage(key, state, usageCounter);
 390    }
 391    } else {
 392  0 if (log.isErrorEnabled()) {
 393  0 log.error("internal error: expected to get a state from key [" + key + "]");
 394    }
 395    }
 396    }
 397    }
 398    }
 399   
 400    /**
 401    * Utility method to check if the specified usage count is zero, and if so remove the corresponding EntryUpdateState from the updateStates. This is designed to factor common code.
 402    *
 403    * Warning: This method should always be called while holding both the updateStates field and the state parameter
 404    * @throws Exception
 405    */
 406  4024305 private void checkEntryStateUpdateUsage(String key, EntryUpdateState state, int usageCounter) {
 407    //Clean up the updateStates map to avoid a memory leak once no thread is using this EntryUpdateState instance anymore.
 408  4024305 if (usageCounter ==0) {
 409  15453 EntryUpdateState removedState = (EntryUpdateState) updateStates.remove(key);
 410  15453 if (state != removedState) {
 411  0 if (log.isErrorEnabled()) {
 412  0 log.error("internal error: removed state [" + removedState + "] from key [" + key + "] whereas we expected [" + state + "]");
 413  0 try {
 414  0 throw new Exception("states not equal");
 415    } catch (Exception e) {
 416    // TODO Auto-generated catch block
 417  0 e.printStackTrace();
 418    }
 419    }
 420    }
 421    }
 422    }
 423   
 424    /**
 425    * Flush all entries in the cache on the given date/time.
 426    *
 427    * @param date The date at which all cache entries will be flushed.
 428    */
 429  0 public void flushAll(Date date) {
 430  0 flushAll(date, null);
 431    }
 432   
 433    /**
 434    * Flush all entries in the cache on the given date/time.
 435    *
 436    * @param date The date at which all cache entries will be flushed.
 437    * @param origin The origin of this flush request (optional)
 438    */
 439  0 public void flushAll(Date date, String origin) {
 440  0 flushDateTime = date;
 441   
 442  0 if (listenerList.getListenerCount() > 0) {
 443  0 dispatchCachewideEvent(CachewideEventType.CACHE_FLUSHED, date, origin);
 444    }
 445    }
 446   
 447    /**
 448    * Flush the cache entry (if any) that corresponds to the cache key supplied.
 449    * This call will flush the entry from the cache and remove the references to
 450    * it from any cache groups that it is a member of. On completion of the flush,
 451    * a <tt>CacheEntryEventType.ENTRY_FLUSHED</tt> event is fired.
 452    *
 453    * @param key The key of the entry to flush
 454    */
 455  0 public void flushEntry(String key) {
 456  0 flushEntry(key, null);
 457    }
 458