com.tangosol.net.cache.OldCache Maven / Gradle / Ivy
Show all versions of coherence Show documentation
/*
* Copyright (c) 2000, 2023, Oracle and/or its affiliates.
*
* Licensed under the Universal Permissive License v 1.0 as shown at
* https://oss.oracle.com/licenses/upl.
*/
package com.tangosol.net.cache;
import com.tangosol.util.Base;
import com.tangosol.util.BitHelper;
import com.tangosol.util.Filter;
import com.tangosol.util.FilterEnumerator;
import com.tangosol.util.LiteSet;
import com.tangosol.util.LongArray;
import com.tangosol.util.MapEvent;
import com.tangosol.util.MapListener;
import com.tangosol.util.MapListenerSupport;
import com.tangosol.util.NullImplementation;
import com.tangosol.util.ObservableMap;
import com.tangosol.util.SafeHashMap;
import com.tangosol.util.SparseArray;
import java.lang.reflect.Array;
import java.sql.Time;
import java.util.ArrayList;
import java.util.Collection;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
/**
* A generic cache manager.
*
* The implementation is thread safe and uses a combination of
* Most Recently Used (MRU) and Most Frequently Used (MFU) caching
* strategies.
*
* The cache is size-limited, which means that once it reaches its maximum
* size ("high-water mark") it prunes itself (to its "low-water mark"). The
* cache high- and low-water-marks are measured in terms of "units", and each
* cached item by default uses one unit. All of the cache constructors, except
* for the default constructor, require the maximum number of units to be
* passed in. To change the number of units that each cache entry uses, either
* set the Units property of the cache entry, or extend the Cache
* implementation so that the inner Entry class calculates its own unit size.
* To determine the current, high-water and low-water sizes of the cache, use
* the cache object's Units, HighUnits and LowUnits properties. The HighUnits
* and LowUnits properties can be changed, even after the cache is in use.
* To specify the LowUnits value as a percentage when constructing the cache,
* use the extended constructor taking the percentage-prune-level.
*
* Each cached entry expires after one hour by default. To alter this
* behavior, use a constructor that takes the expiry-millis; for example, an
* expiry-millis value of 10000 will expire entries after 10 seconds. The
* ExpiryDelay property can also be set once the cache is in use, but it
* will not affect the expiry of previously cached items.
*
* Cache hit statistics can be obtained from the CacheHits, CacheMisses,
* HitProbability, KeyHitProbability and CompositeHitProbability read-only
* properties. The statistics can be reset by invoking resetHitStatistics.
* The statistics are automatically reset when the cache is cleared (the clear
* method).
*
* The OldCache implements the ObservableMap interface, meaning it provides
* event notifications to any interested listener for each insert, update and
* delete, including those that occur when the cache is pruned or entries
* are automatically expired.
*
* This implementation is designed to support extension through inheritance.
* When overriding the inner Entry class, the OldCache.instantiateEntry
* factory method must be overridden to instantiate the correct Entry
* sub-class. To override the one-unit-per-entry default behavior, extend the
* inner Entry class and override the calculateUnits method.
*
* @author cp 2001.04.19
* @author cp 2005.05.18 moved to com.tangosol.net.cache package
*
* @deprecated As of Coherence 3.1, use {@link LocalCache} instead
*/
public class OldCache
extends SafeHashMap
implements ObservableMap, ConfigurableCacheMap
{
// ----- constructors ---------------------------------------------------
/**
* Construct the cache manager.
*/
public OldCache()
{
this(DEFAULT_UNITS);
}
/**
* Construct the cache manager.
*
* @param cUnits the number of units that the cache manager will cache
* before pruning the cache
*/
public OldCache(int cUnits)
{
this(cUnits, DEFAULT_EXPIRE);
}
/**
* Construct the cache manager.
*
* @param cUnits the number of units that the cache manager will
* cache before pruning the cache
* @param cExpiryMillis the number of milliseconds that each cache entry
* lives before being automatically expired
*/
public OldCache(int cUnits, int cExpiryMillis)
{
this(cUnits, cExpiryMillis, DEFAULT_PRUNE);
}
/**
* Construct the cache manager.
*
* @param cUnits the number of units that the cache manager will
* cache before pruning the cache
* @param cExpiryMillis the number of milliseconds that each cache entry
* lives before being automatically expired
* @param dflPruneLevel the percentage of the total number of units that
* will remain after the cache manager prunes the
* cache (i.e. this is the "low water mark" value);
* this value is in the range 0.0 to 1.0
*/
public OldCache(int cUnits, int cExpiryMillis, double dflPruneLevel)
{
m_dflPruneLevel = Math.min(Math.max(dflPruneLevel, 0.0), 0.99);
setHighUnits(cUnits);
m_cExpiryDelay = Math.max(cExpiryMillis, 0);
}
// ----- Map interface --------------------------------------------------
/**
* {@inheritDoc}
*/
public int size()
{
// check if the cache needs flushing
evict();
return super.size();
}
/**
* {@inheritDoc}
*/
public boolean isEmpty()
{
// this will call evict()
return size() == 0;
}
/**
* {@inheritDoc}
*/
public boolean containsKey(Object key)
{
// check if the cache needs flushing
evict();
return getEntryInternal(key) != null;
}
/**
* {@inheritDoc}
*/
public Object get(Object oKey)
{
Map.Entry entry = getEntry(oKey);
return (entry == null ? null : entry.getValue());
}
/**
* {@inheritDoc}
*/
public SafeHashMap.Entry getEntry(Object oKey)
{
// check if the cache needs flushing
evict();
Entry entry = (Entry) getEntryInternal(oKey);
if (entry == null)
{
m_stats.registerMiss();
}
else
{
m_stats.registerHit();
entry.touch();
}
return entry;
}
/**
* {@inheritDoc}
*/
public ConfigurableCacheMap.Entry getCacheEntry(Object oKey)
{
return (ConfigurableCacheMap.Entry) getEntry(oKey);
}
/**
* {@inheritDoc}
*/
public Object put(Object oKey, Object oValue)
{
return put(oKey, oValue, 0L);
}
/**
* {@inheritDoc}
*/
public Object put(Object oKey, Object oValue, long cMillis)
{
// check if the cache needs flushing
evict();
Entry entry;
Object oOrig;
synchronized (this)
{
entry = (Entry) getEntryInternal(oKey);
if (entry == null)
{
// new cache entry
oOrig = super.put(oKey, oValue);
}
else
{
// cache entry already exists
entry.touch();
oOrig = entry.setValue(oValue);
}
if (cMillis != 0L)
{
if (entry == null)
{
entry = (Entry) getEntryInternal(oKey);
}
if (entry != null)
{
entry.setExpiryMillis(cMillis > 0L ?
getCurrentTimeMillis() + cMillis : 0L);
}
}
// check the cache size (COH-467, COH-480)
if (m_cCurUnits > m_cMaxUnits)
{
prune();
// could have evicted the item we just inserted/updated
if (getEntryInternal(oKey) == null)
{
oOrig = null;
}
}
}
m_stats.registerPut(0L);
return oOrig;
}
/**
* {@inheritDoc}
*/
public Object remove(Object oKey)
{
// check if the cache needs flushing
evict();
synchronized (this)
{
// determine if the key is in the cache
Entry entry = (Entry) getEntryInternal(oKey);
if (entry == null)
{
return null;
}
else
{
entry.discard();
removeEntryInternal(entry);
return entry.getValue();
}
}
}
/**
* {@inheritDoc}
*/
public synchronized void clear()
{
while (true)
{
try
{
// notify cache entries of their impending removal
for (Entry entry : (Set) entrySet())
{
entry.discard();
}
// verify that the cache maintains its data correctly
if (m_cCurUnits != 0L)
{
// soft assertion
Base.err("Invalid LocalCache unit count after clear: " + m_cCurUnits);
m_cCurUnits = 0L;
}
if (!m_arrayExpiry.isEmpty())
{
// soft assertion
Base.err("LocalCache still contained " + m_arrayExpiry.getSize()
+ " expiry items after clear.");
m_arrayExpiry.clear();
}
break;
}
catch (ConcurrentModificationException e)
{
}
}
// reset the cache storage
super.clear();
// discard any pending evictions
m_iterEvict = null;
// reset hit/miss stats
resetHitStatistics();
}
// ----- ObservableMap methods ------------------------------------------
/**
* {@inheritDoc}
*/
public synchronized void addMapListener(MapListener listener)
{
addMapListener(listener, (Filter) null, false);
}
/**
* {@inheritDoc}
*/
public synchronized void removeMapListener(MapListener listener)
{
removeMapListener(listener, (Filter) null);
}
/**
* {@inheritDoc}
*/
public synchronized void addMapListener(MapListener listener, Object oKey, boolean fLite)
{
Base.azzert(listener != null);
MapListenerSupport support = m_listenerSupport;
if (support == null)
{
support = m_listenerSupport = new MapListenerSupport();
}
support.addListener(listener, oKey, fLite);
}
/**
* {@inheritDoc}
*/
public synchronized void removeMapListener(MapListener listener, Object oKey)
{
Base.azzert(listener != null);
MapListenerSupport support = m_listenerSupport;
if (support != null)
{
support.removeListener(listener, oKey);
if (support.isEmpty())
{
m_listenerSupport = null;
}
}
}
/**
* {@inheritDoc}
*/
public synchronized void addMapListener(MapListener listener, Filter filter, boolean fLite)
{
Base.azzert(listener != null);
MapListenerSupport support = m_listenerSupport;
if (support == null)
{
support = m_listenerSupport = new MapListenerSupport();
}
support.addListener(listener, filter, fLite);
}
/**
* {@inheritDoc}
*/
public synchronized void removeMapListener(MapListener listener, Filter filter)
{
Base.azzert(listener != null);
MapListenerSupport support = m_listenerSupport;
if (support != null)
{
support.removeListener(listener, filter);
if (support.isEmpty())
{
m_listenerSupport = null;
}
}
}
// ----- ConfigurableCacheMap interface ---------------------------------
/**
* {@inheritDoc}
*/
public Map getAll(Collection colKeys)
{
Map map = new HashMap();
for (Object oKey : colKeys)
{
Entry entry = (Entry) getEntry(oKey);
if (entry != null)
{
map.put(oKey, entry.getValue());
}
}
return map;
}
/**
* Evict a specified key from the cache, as if it had expired from the
* cache. If the key is not in the cache or the entry is not eligible
* for eviction, then this method has no effect.
*
* @param oKey the key to evict from the cache
*/
public void evict(Object oKey)
{
Entry entry = (Entry) getEntryInternal(oKey);
if (entry != null)
{
removeEvicted(entry);
}
}
/**
* Evict the specified keys from the cache, as if they had each expired
* from the cache.
*
* The result of this method is defined to be semantically the same as
* the following implementation:
*
*
* for (Iterator iter = colKeys.iterator(); iter.hasNext(); )
* {
* Object oKey = iter.next();
* evict(oKey);
* }
*
*
* @param colKeys a collection of keys to evict from the cache
*/
public void evictAll(Collection colKeys)
{
for (Object oKey : colKeys)
{
Entry entry = (Entry) getEntryInternal(oKey);
if (entry != null)
{
removeEvicted(entry);
}
}
}
/**
* Evict all entries from the cache that are no longer valid, and
* potentially prune the cache size if the cache is size-limited
* and its size is above the caching low water mark.
*/
public void evict()
{
// check if flushing has been done recently
long lCurrent = getCurrentTimeMillis();
if (lCurrent > m_lNextFlush)
{
// protect against other threads attempting to evict() at the
// same time
synchronized (this)
{
if (lCurrent > m_lNextFlush && m_apprvrEvict != EvictionApprover.DISAPPROVER)
{
// protect against _this_ thread attempting to evict()
// at the same time (e.g. recursively as the side-effect
// of an event)
m_lNextFlush = Long.MAX_VALUE;
try
{
Set setEvict = null;
LongArray arrayExpiry = m_arrayExpiry;
synchronized (arrayExpiry)
{
if (!arrayExpiry.isEmpty() && lCurrent > arrayExpiry.getFirstIndex())
{
for (LongArray.Iterator iterKeySets = arrayExpiry.iterator();
iterKeySets.hasNext(); )
{
Set setKeys = (Set) iterKeySets.next();
if (setKeys != null && lCurrent > iterKeySets.getIndex())
{
iterKeySets.remove();
if (setEvict == null)
{
setEvict = setKeys;
}
else
{
setEvict.addAll(setKeys);
}
}
else
{
break;
}
}
}
}
if (setEvict != null)
{
evictAll(setEvict);
}
}
finally
{
// don't allow another flush for a quarter second
// (the expiry has a quarter-second granularity; see
// setExpiryMillis)
m_lNextFlush = getCurrentTimeMillis() + 0x100L;
}
}
}
}
}
/**
* Returns the CacheStatistics for this cache.
*
* @return a CacheStatistics object
*/
public CacheStatistics getCacheStatistics()
{
return m_stats;
}
// ----- inner class: EntrySet ------------------------------------------
/**
* Factory pattern.
*
* @return a new instance of the EntrySet class (or a subclass thereof)
*/
protected SafeHashMap.EntrySet instantiateEntrySet()
{
return new EntrySet();
}
/**
* A set of entries backed by this map.
*/
protected class EntrySet
extends SafeHashMap.EntrySet
{
// ----- Set interface ------------------------------------------
/**
* Returns an iterator over the elements contained in this collection.
*
* @return an iterator over the elements contained in this collection.
*/
public Iterator iterator()
{
// optimization
if (OldCache.this.isEmpty())
{
return NullImplementation.getIterator();
}
// complete entry set iterator
Iterator iter = instantiateIterator();
// filter to get rid of expired objects
Filter filter = (Entry entry) -> !removeIfExpired(entry);
return new FilterEnumerator(iter, filter);
}
/**
* Returns an array with a runtime type is that of the specified array and
* that contains all of the elements in this collection. If the
* collection fits in the specified array, it is returned therein.
* Otherwise, a new array is allocated with the runtime type of the
* specified array and the size of this collection.
*
* If the collection fits in the specified array with room to spare (i.e.,
* the array has more elements than the collection), the element in the
* array immediately following the end of the collection is set to
* null. This is useful in determining the length of the
* collection only if the caller knows that the collection does
* not contain any null elements.)
*
* @param ao the array into which the elements of the collection are to
* be stored, if it is big enough; otherwise, a new array of the
* same runtime type is allocated for this purpose
*
* @return an array containing the elements of the collection
*
* @throws ArrayStoreException if the runtime type of the specified array
* is not a supertype of the runtime type of every element in this
* collection
*/
public Object[] toArray(Object ao[])
{
Object[] aoAll = super.toArray(ao);
int cAll = aoAll.length;
int ofSrc = 0;
int ofDest = 0;
while (ofSrc < cAll)
{
Entry entry = (Entry) aoAll[ofSrc];
if (entry == null)
{
// this happens when ao is passed in and is larger than
// the number of entries
break;
}
else if (removeIfExpired(entry))
{
//no-op
}
else
{
if (ofSrc > ofDest)
{
aoAll[ofDest] = aoAll[ofSrc];
}
++ofDest;
}
++ofSrc;
}
if (ofSrc == ofDest)
{
// no entries expired; return the original array
return aoAll;
}
if (ao == aoAll)
{
// this is the same array as was passed in; per the toArray
// contract, null the element past the end of the non-expired
// entries (since we removed at least one entry) and return it
ao[ofDest] = null;
return ao;
}
// resize has to occur because we've removed some of the
// entries because they were expired
if (ao == null)
{
ao = new Object[ofDest];
}
else
{
ao = (Object[]) Array.newInstance(ao.getClass().getComponentType(), ofDest);
}
System.arraycopy(aoAll, 0, ao, 0, ofDest);
return ao;
}
}
// ----- inner class: KeySet --------------------------------------------
/**
* Factory pattern.
*
* @return a new instance of the KeySet class (or subclass thereof)
*/
protected SafeHashMap.KeySet instantiateKeySet()
{
return new KeySet();
}
/**
* A set of entries backed by this map.
*/
protected class KeySet
extends SafeHashMap.KeySet
{
// ----- Set interface ------------------------------------------
/**
* Returns an array with a runtime type is that of the specified array and
* that contains all of the elements in this collection. If the
* collection fits in the specified array, it is returned therein.
* Otherwise, a new array is allocated with the runtime type of the
* specified array and the size of this collection.
*
* If the collection fits in the specified array with room to spare (i.e.,
* the array has more elements than the collection), the element in the
* array immediately following the end of the collection is set to
* null. This is useful in determining the length of the
* collection only if the caller knows that the collection does
* not contain any null elements.)
*
* @param ao the array into which the elements of the collection are to
* be stored, if it is big enough; otherwise, a new array of the
* same runtime type is allocated for this purpose
*
* @return an array containing the elements of the collection
*
* @throws ArrayStoreException if the runtime type of the specified array
* is not a supertype of the runtime type of every element in this
* collection
*/
public Object[] toArray(Object ao[])
{
// build list of non-expired keys
Object[] aoAll;
int cAll = 0;
// synchronizing prevents add/remove, keeping size() constant
OldCache map = OldCache.this;
synchronized (map)
{
// create the array to store the map keys
int c = map.size();
aoAll = new Object[c];
if (c > 0)
{
// walk all buckets
SafeHashMap.Entry[] aeBucket = map.m_aeBucket;
for (SafeHashMap.Entry e : aeBucket)
{
// walk all entries in the bucket
Entry entry = (Entry) e;
while (entry != null)
{
if (removeIfExpired(entry))
{
//no-op
}
else
{
aoAll[cAll++] = entry.getKey();
}
entry = entry.getNext();
}
}
}
}
// if no entries had expired, just return the "work" array
if (ao == null && cAll == aoAll.length)
{
return aoAll;
}
// allocate the necessary array (or stick the null in at the
// right place) per the Map spec
if (ao == null)
{
ao = new Object[cAll];
}
else if (ao.length < cAll)
{
ao = (Object[]) Array.newInstance(ao.getClass().getComponentType(), cAll);
}
else if (ao.length > cAll)
{
ao[cAll] = null;
}
// copy the data into the array to return and return it
if (cAll > 0)
{
System.arraycopy(aoAll, 0, ao, 0, cAll);
}
return ao;
}
/**
* {@inheritDoc}
*/
public int size()
{
// COH-1089: get the size value without causing any eviction
// (see SafeHashMap#size)
return OldCache.super.size();
}
}
// ----- inner class: ValuesCollection ----------------------------------
/**
* Factory pattern.
*
* @return a new instance of the ValuesCollection class (or subclass
* thereof)
*/
protected SafeHashMap.ValuesCollection instantiateValuesCollection()
{
return new ValuesCollection();
}
/**
* A collection of values backed by this map.
*/
protected class ValuesCollection
extends SafeHashMap.ValuesCollection
{
// ----- Collection interface -----------------------------------
/**
* Returns an array with a runtime type is that of the specified array and
* that contains all of the elements in this collection. If the
* collection fits in the specified array, it is returned therein.
* Otherwise, a new array is allocated with the runtime type of the
* specified array and the size of this collection.
*
* If the collection fits in the specified array with room to spare (i.e.,
* the array has more elements than the collection), the element in the
* array immediately following the end of the collection is set to
* null. This is useful in determining the length of the
* collection only if the caller knows that the collection does
* not contain any null elements.)
*
* @param ao the array into which the elements of the collection are to
* be stored, if it is big enough; otherwise, a new array of the
* same runtime type is allocated for this purpose
*
* @return an array containing the elements of the collection
*
* @throws ArrayStoreException if the runtime type of the specified array
* is not a supertype of the runtime type of every element in this
* collection
*/
public Object[] toArray(Object ao[])
{
// build list of non-expired values
Object[] aoAll;
int cAll = 0;
// synchronizing prevents add/remove, keeping size() constant
OldCache map = OldCache.this;
synchronized (map)
{
// create the array to store the map values
int c = map.size();
aoAll = new Object[c];
if (c > 0)
{
// walk all buckets
SafeHashMap.Entry[] aeBucket = map.m_aeBucket;
for (SafeHashMap.Entry e : aeBucket)
{
// walk all entries in the bucket
Entry entry = (Entry) e;
while (entry != null)
{
if (removeIfExpired(entry))
{
//no-op
}
else
{
aoAll[cAll++] = entry.getValue();
}
entry = entry.getNext();
}
}
}
}
// if no entries had expired, just return the "work" array
if (ao == null && cAll == aoAll.length)
{
return aoAll;
}
// allocate the necessary array (or stick the null in at the
// right place) per the Map spec
if (ao == null)
{
ao = new Object[cAll];
}
else if (ao.length < cAll)
{
ao = (Object[]) Array.newInstance(ao.getClass().getComponentType(), cAll);
}
else if (ao.length > cAll)
{
ao[cAll] = null;
}
// copy the data into the array to return and return it
if (cAll > 0)
{
System.arraycopy(aoAll, 0, ao, 0, cAll);
}
return ao;
}
}
// ----- Object methods -------------------------------------------------
/**
* For debugging purposes, format the contents of the cache as a String.
*
* @return a String representation of the cache contents
*/
public synchronized String toString()
{
while (true)
{
try
{
StringBuilder sb = new StringBuilder("Cache {\n");
int i = 0;
for (Object entry : entrySet())
{
sb.append('[')
.append(i++)
.append("]: ")
.append(entry)
.append('\n');
}
sb.append('}');
return sb.toString();
}
catch (ConcurrentModificationException e)
{
}
}
}
// ----- Cache management methods ---------------------------------------
/**
* {@inheritDoc}
*/
public int getUnits()
{
return toExternalUnits(m_cCurUnits, getUnitFactor());
}
/**
* {@inheritDoc}
*/
public int getHighUnits()
{
return toExternalUnits(m_cMaxUnits, getUnitFactor());
}
/**
* {@inheritDoc}
*/
public synchronized void setHighUnits(int cMax)
{
long cUnits = toInternalUnits(cMax, getUnitFactor());
m_cMaxUnits = cUnits;
m_cPruneUnits = cUnits == Long.MAX_VALUE ? cUnits : (long) (m_dflPruneLevel * cUnits);
if (m_cCurUnits > cUnits)
{
prune();
}
}
/**
* {@inheritDoc}
*/
public int getLowUnits()
{
return toExternalUnits(m_cPruneUnits, getUnitFactor());
}
/**
* {@inheritDoc}
*/
public synchronized void setLowUnits(int cMin)
{
long cUnits = toInternalUnits(cMin, getUnitFactor());
long cMax = m_cMaxUnits;
if (cUnits >= cMax)
{
cUnits = (long) (m_dflPruneLevel * cMax);
}
else if (cMax == Long.MAX_VALUE)
{
// no max indicates no min
cUnits = cMax;
}
m_cPruneUnits = cUnits;
}
/**
* {@inheritDoc}
*/
public int getUnitFactor()
{
return m_nUnitFactor;
}
/**
* {@inheritDoc}
*/
public void setUnitFactor(int nFactor)
{
if (nFactor == m_nUnitFactor)
{
return;
}
if (nFactor < 1)
{
throw new IllegalArgumentException("unit factor must be >= 1");
}
if (m_cCurUnits > 0)
{
throw new IllegalStateException(
"unit factor cannot be set after the cache has been populated");
}
// only adjust the max units if there was no unit factor set previously
if (m_nUnitFactor == 1 && m_cMaxUnits != Long.MAX_VALUE)
{
m_cMaxUnits *= nFactor;
m_cPruneUnits *= nFactor;
}
m_nUnitFactor = nFactor;
}
/**
* Convert from an external 32-bit unit value to an internal 64-bit unit
* value using the configured units factor.
*
* @param cUnits an external 32-bit units value
* @param nFactor the unit factor
*
* @return an internal 64-bit units value
*/
protected static long toInternalUnits(int cUnits, int nFactor)
{
return cUnits <= 0 || cUnits == Integer.MAX_VALUE
? Long.MAX_VALUE
: ((long) cUnits) * nFactor;
}
/**
* Convert from an internal 64-bit unit value to an external 32-bit unit
* value using the configured units factor.
*
* @param cUnits an internal 64-bit units value
* @param nFactor the unit factor
*
* @return an external 32-bit units value
*/
protected static int toExternalUnits(long cUnits, int nFactor)
{
if (cUnits == 0L || cUnits == Long.MAX_VALUE)
{
return 0;
}
if (nFactor > 1)
{
cUnits = (cUnits + nFactor - 1) / nFactor;
}
return cUnits > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) cUnits;
}
/**
* Determine the current eviction type.
*
* @return one of the EVICTION_POLICY_* enumerated values
*/
public int getEvictionType()
{
return m_nEvictionType;
}
/**
* Specify the eviction type for the cache. The type can only be
* set to an external policy if an EvictionPolicy object has been
* provided.
*
* @param nType one of the EVICTION_POLICY_* enumerated values
*/
public synchronized void setEvictionType(int nType)
{
configureEviction(nType, null);
}
/**
* {@inheritDoc}
*/
public ConfigurableCacheMap.EvictionPolicy getEvictionPolicy()
{
ConfigurableCacheMap.EvictionPolicy policy = m_policy;
if (policy == null)
{
switch (getEvictionType())
{
default:
case EVICTION_POLICY_HYBRID:
policy = INSTANCE_HYBRID;
break;
case EVICTION_POLICY_LRU:
policy = INSTANCE_LRU;
break;
case EVICTION_POLICY_LFU:
policy = INSTANCE_LFU;
break;
}
}
return policy;
}
/**
* {@inheritDoc}
*/
public synchronized void setEvictionPolicy(ConfigurableCacheMap.EvictionPolicy policy)
{
int nType = (policy == null ? EVICTION_POLICY_HYBRID
: EVICTION_POLICY_EXTERNAL);
configureEviction(nType, policy);
}
/**
* {@inheritDoc}
*/
public ConfigurableCacheMap.EvictionApprover getEvictionApprover()
{
return m_apprvrEvict;
}
/**
* {@inheritDoc}
*/
public synchronized void setEvictionApprover(ConfigurableCacheMap.EvictionApprover approver)
{
m_apprvrEvict = approver;
}
/**
* Determine the current unit calculator type.
*
* @return one of the UNIT_CALCULATOR_* enumerated values
*/
public int getUnitCalculatorType()
{
return m_nCalculatorType;
}
/**
* Specify the unit calculator type for the cache. The type can only be
* set to an external unit calculator if a UnitCalculator object has been
* provided.
*
* @param nType one of the UNIT_CALCULATOR_* enumerated values
*/
public void setUnitCalculatorType(int nType)
{
configureUnitCalculator(nType, null);
}
/**
* {@inheritDoc}
*/
public ConfigurableCacheMap.UnitCalculator getUnitCalculator()
{
ConfigurableCacheMap.UnitCalculator calculator = m_calculator;
if (calculator == null)
{
calculator = getUnitCalculatorType() == UNIT_CALCULATOR_BINARY
? BinaryMemoryCalculator.INSTANCE
: InternalUnitCalculator.INSTANCE;
}
return calculator;
}
/**
* {@inheritDoc}
*/
public void setUnitCalculator(ConfigurableCacheMap.UnitCalculator calculator)
{
int nType = (calculator == null
? UNIT_CALCULATOR_FIXED
: UNIT_CALCULATOR_EXTERNAL);
configureUnitCalculator(nType, calculator);
}
/**
* {@inheritDoc}
*/
public int getExpiryDelay()
{
return m_cExpiryDelay;
}
/**
* {@inheritDoc}
*/
public void setExpiryDelay(int cMillis)
{
m_cExpiryDelay = Math.max(cMillis, 0);
}
/**
* {@inheritDoc}
*/
public long getNextExpiryTime()
{
LongArray arrayExpiry = m_arrayExpiry;
return arrayExpiry.isEmpty() ? 0 : arrayExpiry.getFirstIndex();
}
/**
* Determine the date/time at which the next cache flush is scheduled.
* Note that the date/time may be Long.MAX_VALUE, which implies that a
* flush will never occur. Also note that the cache may internally adjust
* the flush time to prevent a flush from occurring during certain
* processing as a means to raise concurrency.
*
* @return the date/time value, in milliseconds, when the cache will next
* automatically flush
*
* @deprecated as of Coherence 3.5
*/
public long getFlushTime()
{
return 0L;
}
/**
* Specify the date/time at which the next cache flush is to occur.
* Note that the date/time may be Long.MAX_VALUE, which implies that a
* flush will never occur. A time in the past or at the present will
* cause an immediate flush.
*
* @param lMillis the date/time value, in milliseconds, when the cache
* should next automatically flush
*
* @deprecated as of Coherence 3.5
*/
public void setFlushTime(long lMillis)
{
// no-op
}
/**
* Determine if incremental eviction is enabled. (Incremental eviction is
* not supported for custom eviction policies.)
*
* @return true if eviction is incremental; false if it is done in bulk
*
* @since Coherence 3.5
*/
public boolean isIncrementalEviction()
{
return m_fIncrementalEvict;
}
/**
* Specify whether incremental eviction is enabled.
*
* @param fIncrementalEvict pass true toe enable incremental eviction;
* false to disable incremental eviction
*
* @since Coherence 3.5
*/
public synchronized void setIncrementalEviction(boolean fIncrementalEvict)
{
m_fIncrementalEvict = fIncrementalEvict;
if (!fIncrementalEvict)
{
m_iterEvict = null;
}
}
// ----- statistics -----------------------------------------------------
/**
* Determine the rough number of cache hits since the cache statistics
* were last reset.
*
* @return the number of {@link #get} calls that have been served by
* existing cache entries
*/
public long getCacheHits()
{
return m_stats.getCacheHits();
}
/**
* Determine the rough number of cache misses since the cache statistics
* were last reset.
*
* @return the number of {@link #get} calls that failed to find an
* existing cache entry because the requested key was not in the
* cache
*/
public long getCacheMisses()
{
return m_stats.getCacheMisses();
}
/**
* Determine the rough probability (0 <= p <= 1) that any particular
* {@link #get} invocation will be satisfied by an existing entry in
* the cache, based on the statistics collected since the last reset
* of the cache statistics.
*
* @return the cache hit probability (0 <= p <= 1)
*/
public double getHitProbability()
{
return m_stats.getHitProbability();
}
/**
* Reset the cache statistics.
*/
public void resetHitStatistics()
{
m_stats.resetHitStatistics();
}
// ----- internal methods -----------------------------------------------
/**
* Configure the eviction type and policy.
*
* @param nType one of the EVICTION_POLICY_* enumerated values
* @param policy an external eviction policy, or null
*/
protected synchronized void configureEviction(
int nType, ConfigurableCacheMap.EvictionPolicy policy)
{
switch (nType)
{
case EVICTION_POLICY_HYBRID:
case EVICTION_POLICY_LRU:
case EVICTION_POLICY_LFU:
policy = null;
break;
case EVICTION_POLICY_EXTERNAL:
if (policy == null)
{
// just use the default
nType = EVICTION_POLICY_HYBRID;
}
else if (policy instanceof InternalEvictionPolicy)
{
nType = ((InternalEvictionPolicy) policy).getEvictionType();
policy = null;
}
break;
default:
throw new IllegalArgumentException("unknown eviction type: " + nType);
}
ConfigurableCacheMap.EvictionPolicy policyPrev = m_policy;
if (policyPrev instanceof MapListener)
{
removeMapListener((MapListener) policyPrev);
}
m_nEvictionType = nType;
m_policy = policy;
m_iterEvict = null;
if (policy instanceof MapListener)
{
addMapListener((MapListener) policy);
}
}
/**
* Configure the unit calculator type and implementation.
*
* @param nType one of the UNIT_CALCULATOR_* enumerated values
* @param calculator an external unit calculator, or null
*/
protected synchronized void configureUnitCalculator(
int nType, ConfigurableCacheMap.UnitCalculator calculator)
{
switch (nType)
{
case UNIT_CALCULATOR_EXTERNAL:
if (calculator == null)
{
// just use the default
nType = UNIT_CALCULATOR_FIXED;
}
else if (calculator == InternalUnitCalculator.INSTANCE)
{
nType = UNIT_CALCULATOR_FIXED;
calculator = null;
}
else if (calculator == BinaryMemoryCalculator.INSTANCE)
{
nType = UNIT_CALCULATOR_BINARY;
calculator = null;
}
else if (UNIT_CALCULATOR_EXTERNAL == m_nCalculatorType &&
Base.equals(calculator, m_calculator))
{
// nothing to do
return;
}
else
{
break;
}
// fall through
case UNIT_CALCULATOR_FIXED:
case UNIT_CALCULATOR_BINARY:
if (nType == m_nCalculatorType)
{
// nothing to do
return;
}
break;
default:
throw new IllegalArgumentException(
"unknown unit calculator type: " + nType);
}
m_nCalculatorType = nType;
m_calculator = calculator;
// recalculate unit costs
for (Iterator iter = entrySet().iterator(); iter.hasNext(); )
{
Entry entry = (Entry) iter.next();
int cUnits = entry.calculateUnits(entry.getValue());
// update both the entry unit count and total unit count
entry.setUnits(cUnits);
}
}
/**
* Locate an Entry in the hash map based on its key. If the Entry has
* expired and is eligible for eviction, it is removed from the hash map.
*
* Unlike the {@link #getEntry} method, this method does not flush the cache
* (if necessary) or update cache statistics.
*
* @param oKey the key object to search for
*
* @return the Entry or null if the entry is not found in the hash map or
* has expired
*/
protected SafeHashMap.Entry getEntryInternal(Object oKey)
{
Entry entry = (Entry) super.getEntryInternal(oKey);
if (entry != null && removeIfExpired(entry))
{
entry = null;
}
return entry;
}
/**
* Remove an entry (if it is eligible for eviction) because it has expired.
*
* Note: This method is the same as {@link #removeEvicted(Entry)} and is left
* for backward compatibility.
*
* @param entry the expired cache entry
* @param fRemoveInternal true if the cache entry still needs to be
* removed from the cache
*
* @return true iff the entry was removed
*
* @deprecated use {@link #removeEvicted(Entry)} instead
*/
protected boolean removeExpired(OldCache.Entry entry, boolean fRemoveInternal)
{
return removeEvicted(entry);
}
/**
* Remove an entry (if it is eligible for eviction) because it has expired.
*
* @param entry the expired cache entry
*
* @return true iff the entry was removed
*/
protected synchronized boolean removeEvicted(Entry entry)
{
ConfigurableCacheMap.EvictionApprover appr = m_apprvrEvict;
if (appr == null || appr.isEvictable(entry))
{
entry.discard();
removeEntryInternal(entry);
return true;
}
else
{
return false;
}
}
/**
* Remove an entry if it has expired.
*
* @param entry the entry
*
* @return true iff the entry was actually removed
*/
protected boolean removeIfExpired(Entry entry)
{
if (entry.isExpired() && m_apprvrEvict != EvictionApprover.DISAPPROVER)
{
synchronized (this)
{
if (entry.isExpired())
{
return removeEvicted(entry);
}
}
}
return false;
}
/**
* Adjust current size.
*
* @param cDelta the delta units to adjust to
*/
protected synchronized void adjustUnits(int cDelta)
{
m_cCurUnits += cDelta;
}
/**
* Prune the cache by discarding the lowest priority cache entries.
*/
protected synchronized void prune()
{
long cMax = m_cMaxUnits;
if (m_cCurUnits <= cMax || m_apprvrEvict == EvictionApprover.DISAPPROVER)
{
return;
}
// COH-764: prioritize reclaiming of expired entries
// Note: it is deliberate that prune() calls evict() and *not* vice-versa
// as prune may call into a custom EvictionPolicy that could legally
// call a 'read' method on this map resulting in a call to evict (stack overflow)
evict();
if (m_cCurUnits < cMax)
{
return;
}
// start a new eviction cycle
long ldtStart = getCurrentTimeMillis();
int nType = getEvictionType();
if (nType == EVICTION_POLICY_EXTERNAL)
{
getEvictionPolicy().requestEviction(getLowUnits());
}
else
{
// first attempt to continue a previous incremental eviction
if (m_iterEvict != null)
{
pruneIncremental();
if (m_cCurUnits < cMax)
{
m_stats.registerIncrementalCachePrune(ldtStart);
return;
}
}
long cTarget = m_cPruneUnits;
ArrayList listEvict = null;
long cRemEvict = m_cCurUnits - cTarget;
boolean fLRU = (nType == EVICTION_POLICY_LRU);
// if eviction is being deferred, create a list of items to evict
// (attempting to pre-size the list in such a way that it will
// hold the necessary amount of items without resizing)
if (isIncrementalEviction())
{
double dflEstPct = Math.max(0.01, Math.min(1.0,
1.0 - (((double) cTarget) / (cMax + 1L)) + .005));
listEvict = new ArrayList((int) (dflEstPct * super.size()) + 10);
}
switch (nType)
{
default:
case EVICTION_POLICY_HYBRID:
{
int cLists = 11;
ArrayList[] alist = new ArrayList[cLists];
int cGuess = super.size() >>> 4;
for (int i = 0; i < cLists; ++i)
{
alist[i] = new ArrayList(cGuess);
}
// calculate a rough average number of touches that each
// entry should expect to have
CacheStatistics stats = getCacheStatistics();
m_cAvgTouch = (int) ((stats.getTotalPuts() + stats.getTotalGets())
/ ((super.size() + 1L) * (stats.getCachePrunes() + 1L)));
// sort the entries by their priorities to be retained
SafeHashMap.Entry[] aeBucket = m_aeBucket;
for (SafeHashMap.Entry e : aeBucket)
{
Entry entry = (Entry) e;
while (entry != null)
{
alist[entry.getPriority()].add(entry);
entry = entry.getNext();
}
}
// build a list of the items to evict incrementally,
// from the lowest (10) priority to the highest (0)
// until the cache will drop to its low units
ForEachPriority: for (int i = cLists - 1; i >= 0; --i)
{
for (Object entry : alist[i])
{
cRemEvict -= queueForEviction((Entry) entry, listEvict);
if (cRemEvict <= 0L)
{
break ForEachPriority;
}
}
}
}
break;
case EVICTION_POLICY_LRU:
case EVICTION_POLICY_LFU:
{
SparseArray array = new SparseArray();
// sort the entries by their recentness / frequentness of use
SafeHashMap.Entry[] aeBucket = m_aeBucket;
for (SafeHashMap.Entry e : aeBucket)
{
Entry entry = (Entry) e;
while (entry != null)
{
long lOrder = fLRU ? entry.getLastTouchMillis()
: entry.getTouchCount();
Object oPrev = array.set(lOrder, entry);
if (oPrev != null)
{
// oops, more than one entry with the same order;
// make a list of entries
List list;
if (oPrev instanceof List)
{
list = (List) oPrev;
}
else
{
list = new ArrayList();
list.add(oPrev);
}
list.add(entry);
array.set(lOrder, list);
}
entry = entry.getNext();
}
}
// evict from the least to the most frequently / recently
// used until the cache has dropped below its low units
ForEachEntry:
for (Object o : array)
{
if (o instanceof Entry)
{
cRemEvict -= queueForEviction((Entry) o, listEvict);
if (cRemEvict <= 0L)
{
break;
}
}
else
{
List list = (List) o;
for (Object entry : list)
{
cRemEvict -= queueForEviction((Entry) entry, listEvict);
if (cRemEvict <= 0L)
{
break ForEachEntry;
}
}
}
}
}
break;
}
if (!fLRU)
{
// reset touch counts
SafeHashMap.Entry[] aeBucket = m_aeBucket;
for (SafeHashMap.Entry e : aeBucket)
{
Entry entry = (Entry) e;
while (entry != null)
{
entry.resetTouchCount();
entry = entry.getNext();
}
}
}
// store off the list of pending evictions
if (listEvict != null)
{
m_iterEvict = listEvict.listIterator();
}
// make a first pass at the pending evictions
pruneIncremental();
}
m_stats.registerCachePrune(ldtStart);
m_lLastPrune = getCurrentTimeMillis();
}
/**
* When determining items to evict, they are either evicted immediately or
* their eviction is deferred. This method is responsible for handling
* both of those cases as items are selected for eviction.
*
* @param entry the entry to evict
* @param listEvict the list to defer to if deferring, otherwise null
*
* @return the number of units queued for eviction (or evicted)
*
* @since Coherence 3.5
*/
private int queueForEviction(Entry entry, List listEvict)
{
int cUnits = entry.getUnits();
if (listEvict == null)
{
// try to evict now, if we don't succeed,
// then zero units were queued
if (!removeEvicted(entry))
{
cUnits = 0;
}
}
else
{
// defer eviction
entry.setEvictable(true);
listEvict.add(entry.getKey());
}
return cUnits;
}
/**
* Incrementally evict some entries that were previously selected for
* eviction.
*
* @since Coherence 3.5
*/
private void pruneIncremental()
{
ListIterator iterEvict = m_iterEvict;
if (iterEvict != null)
{
// pruning will proceed until the cache is down below the max
// units, but since there's a cost to this processing, do some
// arbitrary minimum number of evictions while we're here
long cMaxUnits = m_cMaxUnits;
int cMinEntries = 60;
while (iterEvict.hasNext())
{
Entry entry = (Entry) getEntryInternal(iterEvict.next());
iterEvict.set(null); // COH-27922 - release the reference to the entry so that it can be garbage collected
if (entry != null && entry.isEvictable() &&
removeEvicted(entry) &&
--cMinEntries <= 0 && m_cCurUnits < cMaxUnits)
{
return;
}
}
m_iterEvict = null;
}
}
/**
* Check if any entries in the cache have expired, and evict them if they
* have.
*
* @deprecated as of Coherence 3.5, use {@link #evict()}
*/
protected void checkFlush()
{
evict();
}
/**
* Factory pattern: instantiate a new MapEvent corresponding
* to the specified parameters.
*
* @param nId the event Id
* @param oKey the key
* @param oValueOld the old value
* @param oValueNew the new value
*
* @return a new instance of the MapEvent class (or a subclass thereof)
*/
protected MapEvent instantiateMapEvent(
int nId, Object oKey, Object oValueOld, Object oValueNew)
{
return new MapEvent(this, nId, oKey, oValueOld, oValueNew);
}
// ----- event dispatching ----------------------------------------------
/**
* Accessor for the MapListenerSupport for sub-classes.
*
* @return the MapListenerSupport, or null if there are no listeners
*/
protected MapListenerSupport getMapListenerSupport()
{
return m_listenerSupport;
}
/**
* Determine if the OldCache has any listeners at all.
*
* @return true iff this OldCache has at least one MapListener
*/
protected boolean hasListeners()
{
// m_listenerSupport defaults to null, and it is reset to null when
// the last listener unregisters
return m_listenerSupport != null;
}
/**
* Dispatch the passed event.
*
* @param evt a CacheEvent object
*/
protected void dispatchEvent(MapEvent evt)
{
MapListenerSupport listenerSupport = getMapListenerSupport();
if (listenerSupport != null)
{
// the events can only be generated while the current thread
// holds the monitor on this map
synchronized (this)
{
listenerSupport.fireEvent(evt, false);
}
}
}
/**
* Return the current {@link Base#getSafeTimeMillis() safe time} or
* {@link Base#getLastSafeTimeMillis last safe time}
* depending on the optimization flag.
*
* @return the current time
*/
public long getCurrentTimeMillis()
{
return m_fOptimizeGetTime ?
Base.getLastSafeTimeMillis() : Base.getSafeTimeMillis();
}
/**
* Specify whether or not this cache is used in the environment,
* where the {@link Base#getSafeTimeMillis()} is used very frequently and
* as a result, the {@link Base#getLastSafeTimeMillis} could be used
* without sacrificing the clock precision. By default, the optimization
* is off.
*
* @param fOptimize pass true to turn the "last safe time" optimization on
*/
public void setOptimizeGetTime(boolean fOptimize)
{
m_fOptimizeGetTime = fOptimize;
}
// ----- inner class: Entry ---------------------------------------------
/**
* Factory method. This method exists to allow the OldCache class to be
* easily inherited from by allowing the Entry class to be easily
* sub-classed.
*
* @return an instance of Entry that holds the passed cache value
*/
protected SafeHashMap.Entry instantiateEntry()
{
return new Entry();
}
/**
* A holder for a cached value.
*
* @author cp 2001.04.19
*/
public class Entry
extends SafeHashMap.Entry
implements ConfigurableCacheMap.Entry
{
// ----- constructors -------------------------------------------
/**
* Construct the cacheable entry that holds the cached value.
*/
public Entry()
{
m_dtLastUse = m_dtCreated = getCurrentTimeMillis();
}
/**
* This method is invoked when the containing Map has actually
* added this Entry to itself.
*/
protected void onAdd()
{
scheduleExpiry();
// update units
int cNewUnits = calculateUnits(m_oValue);
OldCache map = OldCache.this;
synchronized (map)
{
int cOldUnits = m_cUnits;
if (cOldUnits == -1)
{
// entry is discarded; avoid exception
return;
}
if (cNewUnits != cOldUnits)
{
map.adjustUnits(cNewUnits - cOldUnits);
m_cUnits = cNewUnits;
}
}
// issue add notification
MapListenerSupport support = map.getMapListenerSupport();
if (support != null && !support.isEmpty())
{
map.dispatchEvent(map.instantiateMapEvent(
MapEvent.ENTRY_INSERTED, getKey(), null, getValue()));
}
}
// ----- Map.Entry interface ------------------------------------
/**
* Update the cached value.
*
* @param oValue the new value to cache
*
* @return the old cache value
*/
public Object setValue(Object oValue)
{
// optimization - verify that the entry is still valid
if (m_cUnits == -1)
{
// entry is discarded; avoid exception
super.setValue(oValue);
return null;
}
// perform the entry update
Object oPrev;
int cNewUnits = calculateUnits(oValue);
OldCache map = OldCache.this;
synchronized (map)
{
int cOldUnits = m_cUnits;
if (cOldUnits == -1)
{
// entry is discarded; avoid repetitive events
super.setValue(oValue);
return null;
}
if (cNewUnits != cOldUnits)
{
map.adjustUnits(cNewUnits - cOldUnits);
m_cUnits = cNewUnits;
}
oPrev = super.setValue(oValue);
// if previously queued for eviction, interpret the
// modification as being an indicator that it should not be
// evicted
setEvictable(false);
}
scheduleExpiry();
// issue update notification
if (map.hasListeners())
{
map.dispatchEvent(map.instantiateMapEvent(
MapEvent.ENTRY_UPDATED, getKey(), oPrev, oValue));
}
return oPrev;
}
// ----- SafeHashMap.Entry methods ------------------------------
/**
* {@inheritDoc}
*/
protected void copyFrom(SafeHashMap.Entry entry)
{
Entry entryThat = (Entry) entry;
super.copyFrom(entry);
m_dtCreated = entryThat.m_dtCreated;
m_dtLastUse = entryThat.m_dtLastUse;
m_dtExpiry = entryThat.m_dtExpiry;
m_cUses = entryThat.m_cUses;
m_cUnits = entryThat.m_cUnits;
}
// ----- Cache Entry methods ------------------------------------
/**
* Calculate a cache priority.
*
* @return a value between 0 and 10, 0 being the highest priority
*/
public int getPriority()
{
// calculate an LRU score - how recently was the entry used?
long dtPrune = m_lLastPrune;
long dtTouch = m_dtLastUse;
int nScoreLRU = 0;
if (dtTouch > dtPrune)
{
// measure recentness against the window of time since the
// last prune
long dtCurrent = getCurrentTimeMillis();
long cMillisDormant = dtCurrent - dtTouch;
long cMillisWindow = dtCurrent - dtPrune;
double dflPct = (cMillisWindow - cMillisDormant) / (1.0 + cMillisWindow);
nScoreLRU = 1 + BitHelper.indexOfMSB((int) ((dflPct * dflPct * 64)));
}
// calculate "frequency" - how often has the entry been used?
int cUses = m_cUses;
int nScoreLFU = 0;
if (cUses > 0)
{
nScoreLFU = 1;
int cAvg = m_cAvgTouch;
if (cUses > cAvg)
{
++nScoreLFU;
}
int cAdj = (cUses << 1) - cAvg;
if (cAdj > 0)
{
nScoreLFU += 1 + Math.min(4,
BitHelper.indexOfMSB((int) ((cAdj << 3) / (1.0 + cAvg))));
}
}
// use comparison to another entry as a bonus score
Entry entryNext = getNext();
if (entryNext != null)
{
if (dtTouch > entryNext.m_dtLastUse)
{
++nScoreLRU;
}
if (cUses > entryNext.m_cUses)
{
++nScoreLFU;
}
}
return Math.max(0, 10 - nScoreLRU - nScoreLFU);
}
/**
* Determine when the cache entry was created.
*
* @return the date/time value, in millis, when the entry was created
*/
public long getCreatedMillis()
{
return m_dtCreated;
}
/**
* Called each time the entry is accessed or modified.
*/
public void touch()
{
++m_cUses;
m_dtLastUse = getCurrentTimeMillis();
ConfigurableCacheMap.EvictionPolicy policy = OldCache.this.m_policy;
if (policy != null)
{
policy.entryTouched(this);
}
}
/**
* Determine when the cache entry was last touched.
*
* @return the date/time value, in millis, when the entry was most
* recently touched
*/
public long getLastTouchMillis()
{
return m_dtLastUse;
}
/**
* Determine the number of times that the cache entry has been touched.
*
* @return the number of times that the cache entry has been touched
*/
public int getTouchCount()
{
return m_cUses;
}
/**
* Reset the number of times that the cache entry has been touched.
* The touch count does not get reset to zero, but rather to a
* fraction of its former self; this prevents long lived items from
* gaining an unassailable advantage in the eviction process.
*
* @since Coherence 3.5
*/
protected void resetTouchCount()
{
int cUses = m_cUses;
if (cUses > 0)
{
m_cUses = Math.max(1, cUses >>> 4);
}
}
/**
* Determine when the cache entry will expire, if ever.
*
* @return the date/time value, in millis, when the entry will (or
* did) expire; zero indicates no expiry
*/
public long getExpiryMillis()
{
return m_dtExpiry;
}
/**
* Specify when the cache entry will expire, or disable expiry. Note
* that if the cache is configured for automatic expiry, each
* subsequent update to this cache entry will reschedule the expiry
* time.
*
* @param lMillis pass the date/time value, in millis, for when the
* entry will expire, or pass zero to disable automatic expiry
*/
public void setExpiryMillis(long lMillis)
{
if (lMillis != 0L || m_dtExpiry != 0L)
{
registerExpiry(lMillis);
m_dtExpiry = lMillis;
}
}
/**
* Register (or unregister or replace the registration of) this entry for
* expiry.
*
* @param lMillis the date/time value for when the entry will expire;
* 0 is passed to indicate that the entry needs to be
* removed from the items queued for expiry
*/
protected void registerExpiry(long lMillis)
{
LongArray arrayExpiry = m_arrayExpiry;
synchronized (arrayExpiry)
{
boolean fWasEmpty = arrayExpiry.isEmpty();
// dequeue previous expiry
long lMillisOld = m_dtExpiry;
if (lMillisOld > 0L)
{
// resolution is 1/4 second (to more efficiently lump
// keys into sets)
lMillisOld &= ~0xFFL;
Set setKeys = (Set) arrayExpiry.get(lMillisOld);
if (setKeys != null)
{
setKeys.remove(getKey());
if (setKeys.isEmpty())
{
arrayExpiry.remove(lMillisOld);
}
}
}
// enqueue new expiry
if (lMillis > 0L)
{
lMillis &= ~0xFFL;
Set setKeys = (Set) arrayExpiry.get(lMillis);
if (setKeys == null)
{
setKeys = new LiteSet();
arrayExpiry.set(lMillis, setKeys);
}
setKeys.add(getKey());
// the "next flush" is initially set to "never" (max long)
// to avoid any attempts to flush; now that something is
// scheduled to expire, make sure that flushes are enabled
if (fWasEmpty && m_lNextFlush == Long.MAX_VALUE)
{
m_lNextFlush = 0L;
}
}
}
}
/**
* Determine if the cache entry has expired.
*
* @return true if the cache entry was subject to automatic expiry and
* the current time is greater than the entry's expiry time
*/
public boolean isExpired()
{
long dtExpiry = m_dtExpiry;
return dtExpiry != 0 && dtExpiry < getCurrentTimeMillis();
}
/**
* Reschedule the cache entry expiration.
*/
protected void scheduleExpiry()
{
long dtExpiry = 0L;
int cDelay = OldCache.this.m_cExpiryDelay;
if (cDelay > 0)
{
dtExpiry = getCurrentTimeMillis() + cDelay;
}
setExpiryMillis(dtExpiry);
}
/**
* Called to inform the Entry that it is no longer used.
*/
protected void discard()
{
if (!isDiscarded())
{
if (m_dtExpiry > 0L)
{
// remove this entry from the expiry queue
registerExpiry(0L);
}
OldCache map = OldCache.this;
synchronized (map)
{
int cUnits = m_cUnits;
if (cUnits == -1)
{
// entry is discarded; avoid repetitive events
return;
}
if (cUnits > 0)
{
map.adjustUnits(-cUnits);
}
m_cUnits = -1;
}
// issue remove notification
if (map.hasListeners())
{
map.dispatchEvent(map.instantiateMapEvent(
MapEvent.ENTRY_DELETED, getKey(), getValue(), null));
}
}
}
/**
* Determine if this entry has already been discarded from the cache.
*
* @return true if this entry has been discarded
*/
protected boolean isDiscarded()
{
return m_cUnits == -1;
}
/**
* Calculate a cache cost for the specified object.
*
* The default implementation uses the unit calculator type of the
* containing cache.
*
* @param oValue the cache value to evaluate for unit cost
*
* @return an integer value 0 or greater, with a larger value
* signifying a higher cost
*/
protected int calculateUnits(Object oValue)
{
OldCache map = OldCache.this;
Object oKey = getKey();
switch (map.getUnitCalculatorType())
{
case UNIT_CALCULATOR_BINARY:
return BinaryMemoryCalculator.INSTANCE.calculateUnits(oKey, oValue);
case UNIT_CALCULATOR_EXTERNAL:
return map.m_calculator.calculateUnits(oKey, oValue);
case UNIT_CALCULATOR_FIXED:
default:
return 1;
}
}
/**
* Determine the number of cache units used by this Entry.
*
* @return an integer value 0 or greater, with a larger value
* signifying a higher cost; -1 implies that the Entry
* has been discarded
*/
public int getUnits()
{
return m_cUnits;
}
/**
* Specify the number of cache units used by this Entry.
*
* @param cUnits an integer value 0 or greater, with a larger value
* signifying a higher cost
*/
public void setUnits(int cUnits)
{
azzert(cUnits >= 0);
synchronized (OldCache.this)
{
int cOldUnits = m_cUnits;
if (cOldUnits == -1)
{
// entry is discarded; avoid exception
return;
}
if (cUnits != cOldUnits)
{
OldCache.this.adjustUnits(cUnits - cOldUnits);
m_cUnits = cUnits;
}
}
}
/**
* Determine if this entry has been marked as being evictable.
*
* @return true if this entry is evictable
*
* @since Coherence 3.5
*/
protected boolean isEvictable()
{
return m_fEvictable;
}
/**
* Specify that this entry is evictable or not.
*
* @param fEvict true to specify that this entry is evictable, such
* as when it is selected for deferred eviction, and
* false to specify that it is no longer evictable
*
* @since Coherence 3.5
*/
protected void setEvictable(boolean fEvict)
{
m_fEvictable = fEvict;
}
// ----- Object methods -----------------------------------------
/**
* Render the cache entry as a String.
*
* @return the details about this Entry
*/
public String toString()
{
long dtExpiry = getExpiryMillis();
return super.toString()
+ ", priority=" + getPriority()
+ ", created=" + new Time(getCreatedMillis())
+ ", last-use=" + new Time(getLastTouchMillis())
+ ", expiry=" + (dtExpiry == 0 ? "none"
: new Time(dtExpiry) + (isExpired() ? " (expired)" : ""))
+ ", use-count=" + getTouchCount()
+ ", units=" + getUnits();
}
// ----- internal -----------------------------------------------
/**
* Package Private: Obtain the next cache entry in the chain of
* cache entries for a given hash bucket.
*
* @return the next cache entry in the hash bucket
*/
Entry getNext()
{
return (Entry) m_eNext;
}
/**
* Package Private: Specify the next cache entry in the chain of
* cache entries for a given hash bucket.
*
* @param entry the next cache entry
*/
void setNext(Entry entry)
{
m_eNext = entry;
}
// ----- data members -------------------------------------------
/**
* The time at which this Entry was created.
*/
private volatile long m_dtCreated;
/**
* The time at which this Entry was last accessed.
*/
private volatile long m_dtLastUse;
/**
* The time at which this Entry will (or did) expire.
*/
private volatile long m_dtExpiry;
/**
* The number of times that this Entry has been accessed.
*/
private int m_cUses;
/**
* The number of units for the Entry.
*/
private int m_cUnits;
/**
* This specifies whether or not this entry has been selected For
* deferred eviction.
*/
private boolean m_fEvictable;
}
// ----- interface: EvictionPolicy --------------------------------------
/**
* An eviction policy is an object that the cache provides with access
* information, and when requested, the eviction policy selects and
* evicts entries from the cache. If the eviction policy needs to be
* aware of changes to the cache, it must implement the MapListener
* interface; if it does, it will automatically be registered to receive
* MapEvents.
*/
public interface EvictionPolicy
extends ConfigurableCacheMap.EvictionPolicy
{
}
// ----- inner class: InternalEvictionPolicy ----------------------------
/**
* The InternalEvictionPolicy represents a pluggable eviction policy for
* the non-pluggable built-in (internal) eviction policies supported by
* this cache implementation.
*/
public static class InternalEvictionPolicy
implements EvictionPolicy
{
/**
* Constructor.
*
* @param nType the internal eviction type as defined by the
* EVICTION_POLICY_* constants
*/
InternalEvictionPolicy(int nType)
{
m_nType = nType;
}
/**
* {@inheritDoc}
*/
public void entryTouched(ConfigurableCacheMap.Entry entry)
{
throw new UnsupportedOperationException();
}
/**
* {@inheritDoc}
*/
public void requestEviction(int cMaximum)
{
throw new UnsupportedOperationException();
}
/**
* {@inheritDoc}
*/
public String getName()
{
switch (m_nType)
{
case EVICTION_POLICY_HYBRID:
return "Internal-Hybrid";
case EVICTION_POLICY_LRU:
return "Internal-LRU";
case EVICTION_POLICY_LFU:
return "Internal-LFU";
default:
throw new IllegalStateException();
}
}
/**
* Determine the OldCache eviction type represented by this
* InternalEvictionPolicy.
*
* @return one of the EVICTION_POLICY_* constants
*/
public int getEvictionType()
{
return m_nType;
}
/**
* The OldCache eviction type represented by this
* InternalEvictionPolicy; one of the EVICTION_POLICY_* constants.
*/
private int m_nType;
}
// ----- interface: UnitCalculator --------------------------------------
/**
* A unit calculator is an object that can calculate the cost of caching
* an object.
*/
public interface UnitCalculator
extends ConfigurableCacheMap.UnitCalculator
{
}
// ----- inner class: InternalUnitCalculator ----------------------------
/**
* The InternalUnitCalculator represents a pluggable UnitCalculator for
* the non-pluggable built-in (internal) UnitCalculator implementation
* provided by this cache implementation.
*/
public static class InternalUnitCalculator
implements UnitCalculator
{
/**
* Default constructor.
*/
private InternalUnitCalculator()
{
}
/**
* {@inheritDoc}
*/
public int calculateUnits(Object oKey, Object oValue)
{
return 1;
}
/**
* {@inheritDoc}
*/
public String getName()
{
return "Internal-Fixed";
}
/**
* Singleton instance.
*/
public static final InternalUnitCalculator INSTANCE = new InternalUnitCalculator();
}
// ----- constants ------------------------------------------------------
/**
* By default, the cache size (in units).
*/
public static final int DEFAULT_UNITS = 1000;
/**
* By default, the cache entries expire after one hour.
*/
public static final int DEFAULT_EXPIRE = 3600000;
/**
* By default, expired cache entries are flushed on a minute interval.
*
* @deprecated as of Coherence 3.5
*/
public static final int DEFAULT_FLUSH = 60000;
/**
* By default, when the cache prunes, it reduces its entries to this
* percentage.
*/
public static final double DEFAULT_PRUNE = 0.80;
/**
* By default, the cache prunes based on a hybrid LRU+LFU algorithm.
*/
public static final int EVICTION_POLICY_HYBRID = 0;
/**
* The cache can prune based on a pure Least Recently Used (LRU)
* algorithm.
*/
public static final int EVICTION_POLICY_LRU = 1;
/**
* The cache can prune based on a pure Least Frequently Used (LFU)
* algorithm.
*/
public static final int EVICTION_POLICY_LFU = 2;
/**
* The cache can prune using an external eviction policy.
*/
public static final int EVICTION_POLICY_EXTERNAL = 3;
/**
* Specifies the default unit calculator that weighs all entries equally
* as 1.
*/
public static final int UNIT_CALCULATOR_FIXED = 0;
/**
* Specifies a unit calculator that assigns an object a weight equal to
* the number of bytes of memory required to cache the object.
*
* @see BinaryMemoryCalculator
*/
public static final int UNIT_CALCULATOR_BINARY = 1;
/**
* Specifies a external (custom) unit calculator implementation.
*/
public static final int UNIT_CALCULATOR_EXTERNAL = 2;
// ----- "pluggable" eviction policies and unit calculators -------------
/**
* The EvictionPolicy object for the Hybrid eviction algorithm.
*/
public static final EvictionPolicy INSTANCE_HYBRID = new InternalEvictionPolicy(EVICTION_POLICY_HYBRID);
/**
* The EvictionPolicy object for the Least Recently Used (LRU) eviction
* algorithm.
*/
public static final EvictionPolicy INSTANCE_LRU = new InternalEvictionPolicy(EVICTION_POLICY_LRU);
/**
* The EvictionPolicy object for the Least Frequently Used (LFU) eviction
* algorithm.
*/
public static final EvictionPolicy INSTANCE_LFU = new InternalEvictionPolicy(EVICTION_POLICY_LFU);
/**
* The UnitCalculator object that counts each entry as one unit.
*/
public static final UnitCalculator INSTANCE_FIXED = InternalUnitCalculator.INSTANCE;
/**
* The UnitCalculator object that measures the bytes used by entries. This
* is intended for caches that manage binary data.
*/
public static final UnitCalculator INSTANCE_BINARY = BinaryMemoryCalculator.INSTANCE;
// ----- data members ---------------------------------------------------
/**
* The current number of units in the cache. A unit is an undefined means
* of measuring cached values, and must be 0 or positive. The particular
* Entry implementation being used defines the meaning of unit.
*/
protected volatile long m_cCurUnits;
/**
* The number of units to allow the cache to grow to before pruning.
*/
protected long m_cMaxUnits;
/**
* The percentage of the total number of units that will remain after the
* cache manager prunes the cache (i.e. this is the "low water mark"
* value); this value is in the range 0.0 to 1.0.
*/
protected double m_dflPruneLevel;
/**
* The number of units to prune the cache down to.
*/
protected long m_cPruneUnits;
/**
* The unit factor.
*/
protected int m_nUnitFactor = 1;
/**
* The number of milliseconds that a value will live in the cache.
* Zero indicates no timeout.
*/
protected int m_cExpiryDelay;
/**
* The time before which a expired-entries flush will not be performed.
*/
protected volatile long m_lNextFlush = Long.MAX_VALUE;
/**
* The CacheStatistics object maintained by this cache.
*/
protected SimpleCacheStatistics m_stats = new SimpleCacheStatistics();
/**
* The MapListenerSupport object.
*/
protected MapListenerSupport m_listenerSupport;
/**
* The type of eviction policy employed by the cache; one of the
* EVICTION_POLICY_* enumerated values.
*/
protected int m_nEvictionType = EVICTION_POLICY_HYBRID;
/**
* The eviction policy; for eviction type EVICTION_POLICY_EXTERNAL.
*/
protected ConfigurableCacheMap.EvictionPolicy m_policy;
/**
* The type of unit calculator employed by the cache; one of the
* UNIT_CALCULATOR_* enumerated values.
*/
protected int m_nCalculatorType;
/**
* The external unit calculator.
*/
protected ConfigurableCacheMap.UnitCalculator m_calculator;
/**
* Array of set of keys, indexed by the time of expiry.
* @since Coherence 3.5
*/
protected LongArray m_arrayExpiry = new SparseArray();
/**
* The last time that a prune was run. This value is used by the hybrid
* eviction policy.
* @since Coherence 3.5
*/
protected long m_lLastPrune = getCurrentTimeMillis();
/**
* For a prune cycle, this value is the average number of touches that an
* entry should have. This value is used by the hybrid eviction policy.
* @since Coherence 3.5
*/
protected int m_cAvgTouch;
/**
* For deferred eviction, iterator of entries to evict. If null, then
* there are no entries with deferred eviction.
* @since Coherence 3.5
*/
protected ListIterator m_iterEvict;
/**
* Specifies whether or not this cache will incrementally evict.
*/
protected boolean m_fIncrementalEvict = true;
/**
* The EvictionApprover.
*/
protected ConfigurableCacheMap.EvictionApprover m_apprvrEvict;
/**
* Specifies whether or not this cache is used in the environment,
* where the {@link Base#getSafeTimeMillis()} is used very frequently and
* as a result, the {@link Base#getLastSafeTimeMillis} could be used
* without sacrificing the clock precision. By default, the optimization
* is off.
*/
protected boolean m_fOptimizeGetTime;
}