All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.tangosol.net.cache.LocalCache Maven / Gradle / Ivy

There is a newer version: 24.09
Show newest version
/*
 * 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.ImmutableArrayList;
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.SafeHashMap;
import com.tangosol.util.SimpleEnumerator;
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.Collections;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;


/**
* A LocalCache implementation that supports the JCache API, CacheLoader and
* CacheStore objects.
*
* @since Coherence 2.2
*
* @author cp  2003.05.30
*/
public class LocalCache
        extends SafeHashMap
        implements ConfigurableCacheMap
    {
    // ----- constructors ---------------------------------------------------

    /**
    * Construct the cache manager.
    */
    public LocalCache()
        {
        this(DEFAULT_UNITS);
        }

    /**
    * Construct the cache manager.
    *
    * @param cUnits         the number of units that the cache manager will
    *                       cache before pruning the cache
    */
    public LocalCache(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 LocalCache(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 LocalCache(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);
        }

    /**
    * 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 loader         the CacheLoader or CacheStore to use
    */
    public LocalCache(int cUnits, int cExpiryMillis, CacheLoader loader)
        {
        this(cUnits, cExpiryMillis);

        setCacheLoader(loader);
        }

    // ----- Map interface --------------------------------------------------

    @Override
    public int size()
        {
        // check if the cache needs flushing
        evict();

        return super.size();
        }

    @Override
    public boolean isEmpty()
        {
        // this will call evict()
        return size() == 0;
        }

    @Override
    public boolean containsKey(Object key)
        {
        // check if the cache needs flushing
        tryEvict();

        return getEntryInternal(key) != null;
        }

    @Override
    public Object get(Object oKey)
        {
        Map.Entry entry = getEntry(oKey);
        return (entry == null ? null : entry.getValue());
        }

    @Override
    public ConfigurableCacheMap.Entry getCacheEntry(Object oKey)
        {
        return (ConfigurableCacheMap.Entry) getEntry(oKey);
        }

    @Override
    public Object put(Object oKey, Object oValue)
        {
        return put(oKey, oValue, 0L);
        }

    @Override
    public Object put(Object oKey, Object oValue, long cMillis)
        {
        // check if the cache needs flushing
        tryEvict();

        LocalCache.Entry entry;
        Object oOrig;

        synchronized (this)
            {
            entry = (LocalCache.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 = (LocalCache.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;
        }

    @Override
    public synchronized void clear()
        {
        // this method is only called as a result of a call from the cache
        // consumer, not from any internal eviction etc.

        // if there is a CacheStore, tell it that all entries are being erased
        CacheStore store = getCacheStore();
        if (store != null)
            {
            store.eraseAll(Collections.unmodifiableCollection(keySet()));
            }

        while (true)
            {
            try
                {
                // notify cache entries of their impending removal
                for (LocalCache.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();
        }

    /**
    * Removes the mapping for this key from this map if present.
    *
    * @param oKey  key whose mapping is to be removed from the map
    *
    * @return previous value associated with specified key, or null
    *         if there was no mapping for key.  A null return can
    *         also indicate that the map previously associated null
    *         with the specified key, if the implementation supports
    *         null values
    */
    public synchronized Object remove(Object oKey)
        {
        // this method is only called as a result of a call from the cache
        // consumer, not from any internal eviction etc.

        // check for the specified entry; getEntryInternal() will only return an
        // entry if the entry exists and has not expired
        LocalCache.Entry entry = (LocalCache.Entry) getEntryInternal(oKey);
        if (entry == null)
            {
            return null;
            }
        else
            {
            // if there is a CacheStore, tell it that the entry is being
            // erased
            CacheStore store = getCacheStore();
            if (store != null)
                {
                store.erase(oKey);
                }

            // check if the cache needs flushing
            tryEvict();

            entry.discard();
            removeEntryInternal(entry);
            return entry.getValue();
            }
        }

    // ----- ObservableMap methods ------------------------------------------

    @Override
    public synchronized void addMapListener(MapListener listener)
        {
        addMapListener(listener, (Filter) null, false);
        }

    @Override
    public synchronized void removeMapListener(MapListener listener)
        {
        removeMapListener(listener, (Filter) null);
        }

    @Override
    public void addMapListener(MapListener listener, Object oKey, boolean fLite)
        {
        Base.azzert(listener != null);

        f_listenerLock.lock();
        try
            {
            MapListenerSupport support = m_listenerSupport;
            if (support == null)
                {
                support = m_listenerSupport = new MapListenerSupport();
                }

            support.addListener(listener, oKey, fLite);
            }
        finally
            {
            f_listenerLock.unlock();
            }
        }

    @Override
    public void removeMapListener(MapListener listener, Object oKey)
        {
        Base.azzert(listener != null);

        f_listenerLock.lock();
        try
            {
            MapListenerSupport support = m_listenerSupport;
            if (support != null)
                {
                support.removeListener(listener, oKey);
                if (support.isEmpty())
                    {
                    m_listenerSupport = null;
                    }
                }
            }
        finally
            {
            f_listenerLock.unlock();
            }
        }

    @Override
    public void addMapListener(MapListener listener, Filter filter, boolean fLite)
        {
        Base.azzert(listener != null);

        f_listenerLock.lock();
        try
            {
            MapListenerSupport support = m_listenerSupport;
            if (support == null)
                {
                support = m_listenerSupport = new MapListenerSupport();
                }

            support.addListener(listener, filter, fLite);
            }
        finally
            {
            f_listenerLock.unlock();
            }
        }

    @Override
    public void removeMapListener(MapListener listener, Filter filter)
        {
        Base.azzert(listener != null);

        f_listenerLock.lock();
        try
            {
            MapListenerSupport support = m_listenerSupport;
            if (support != null)
                {
                support.removeListener(listener, filter);
                if (support.isEmpty())
                    {
                    m_listenerSupport = null;
                    }
                }
            }
        finally
            {
            f_listenerLock.unlock();
            }
        }

    // ----- ConfigurableCacheMap interface ---------------------------------

    @Override
    public int getUnits()
        {
        return toExternalUnits(m_cCurUnits, getUnitFactor());
        }

    @Override
    public int getHighUnits()
        {
        return toExternalUnits(m_cMaxUnits, getUnitFactor());
        }

    @Override
    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();
            }
        }

    @Override
    public int getLowUnits()
        {
        return toExternalUnits(m_cPruneUnits, getUnitFactor());
        }

    @Override
    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;
        }

    @Override
    public int getUnitFactor()
        {
        return m_nUnitFactor;
        }

    @Override
    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;
        }

    @Override
    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;
        }

    @Override
    public synchronized void setEvictionPolicy(ConfigurableCacheMap.EvictionPolicy policy)
        {
        int nType = (policy == null ? EVICTION_POLICY_HYBRID
                                    : EVICTION_POLICY_EXTERNAL);
        configureEviction(nType, policy);
        }

    @Override
    public ConfigurableCacheMap.EvictionApprover getEvictionApprover()
        {
        return m_apprvrEvict;
        }

    @Override
    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;
        }

    @Override
    public ConfigurableCacheMap.UnitCalculator getUnitCalculator()
        {
        ConfigurableCacheMap.UnitCalculator calculator = m_calculator;

        if (calculator == null)
            {
            calculator = getUnitCalculatorType() == UNIT_CALCULATOR_BINARY
                         ? BinaryMemoryCalculator.INSTANCE
                         : LocalCache.InternalUnitCalculator.INSTANCE;
            }

        return calculator;
        }
    @Override
    public void setUnitCalculator(ConfigurableCacheMap.UnitCalculator calculator)
        {
        int nType = (calculator == null
                     ? UNIT_CALCULATOR_FIXED
                     : UNIT_CALCULATOR_EXTERNAL);
        configureUnitCalculator(nType, calculator);
        }

    /**
     * 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);
        }

    @Override
    public int getExpiryDelay()
        {
        return m_cExpiryDelay;
        }

    @Override
    public void setExpiryDelay(int cMillis)
        {
        m_cExpiryDelay = Math.max(cMillis, 0);
        }

    @Override
    public long getNextExpiryTime()
        {
        LongArray arrayExpiry = m_arrayExpiry;
        return arrayExpiry.isEmpty() ? 0 : arrayExpiry.getFirstIndex();
        }

    /**
     * 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);
            }
        }

    @Override
    public void evictAll(Collection colKeys)
        {
        for (Object oKey : colKeys)
            {
            LocalCache.Entry entry = (LocalCache.Entry) getEntryInternal(oKey);
            if (entry != null)
                {
                removeEvicted(entry);
                }
            }
        }

    @Override
    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 != ConfigurableCacheMap.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;

                        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;
                        }
                    }
                }
            }
        }

    // ----- JCache interface -----------------------------------------------

    /**
    * Determine the loader used by this LocalCache, if any.
    *
    * @return the loader used by this LocalCache, or null if none
    */
    public CacheLoader getCacheLoader()
        {
        return m_loader;
        }

    /**
    * Specify the loader used by this LocalCache.
    *
    * @param loader  loader to use, or null
    */
    public synchronized void setCacheLoader(CacheLoader loader)
        {
        if (loader != m_loader)
            {
            // unconfigure the old loader
            m_loader    = null;
            m_store     = null;

            MapListener listener = m_listener;
            if (listener != null)
                {
                removeMapListener(listener);
                m_listener = null;
                }

            // configure with the new loader
            m_loader = loader;
            if (loader instanceof CacheStore)
                {
                m_store    = (CacheStore) loader;
                m_listener = listener = instantiateInternalListener();
                this.addMapListener(listener);
                }
            }
        }

    /**
    * Locate an Entry in the hash map based on its key. If the Entry is not in
    * the cache, load the Entry for the specified key and return it.
    *
    * @param oKey  the key to the desired cached Entry
    *
    * @return the Entry corresponding to the specified key, otherwise null
    */
    public SafeHashMap.Entry getEntry(Object oKey)
        {
        // check if the cache needs flushing
        tryEvict();

        Entry entry = (Entry) getEntryInternal(oKey);
        if (entry == null)
            {
            m_stats.registerMiss();
            }
        else
            {
            m_stats.registerHit();
            entry.touch();
            }

        // Try to load and register Misses only if Cache Loader is configured.
        if (entry == null && getCacheLoader() != null)
            {
            long ldtStart = getCurrentTimeMillis();

            load(oKey);

            // use getEntryInternal() instead of get() to avoid screwing
            // up stats
            entry = (Entry) getEntryInternal(oKey);
            m_stats.registerMisses(0, ldtStart);
            }

        return entry;
        }

    /**
    * Get all the specified keys, if they are in the cache. For each key
    * that is in the cache, that key and its corresponding value will be
    * placed in the map that is returned by this method. The absence of
    * a key in the returned map indicates that it was not in the cache,
    * which may imply (for caches that can load behind the scenes) that
    * the requested data could not be loaded.
    * 

* The result of this method is defined to be semantically the same as * the following implementation, without regards to threading issues: * *


    * Map map = new AnyMap(); // could be hash map or ...
    * for (Iterator iter = col.iterator(); iter.hasNext(); )
    *     {
    *     Object oKey = iter.next();
    *     Object oVal = get(oKey);
    *     if (oVal != null || containsKey(oKey))
    *         {
    *         map.put(oKey, oVal);
    *         }
    *     }
    * return map;
    * 
* * @param colKeys a collection of keys that may be in the named cache * * @return a Map of keys to values for the specified keys passed in * col */ public Map getAll(Collection colKeys) { long ldtStart = 0; // first, get all of the requested keys that are already loaded // into the map Map map = peekAll(colKeys); int cTotal = colKeys.size(); int cHits = map.size(); if (cHits < cTotal) { // load the remaining keys CacheLoader loader = getCacheLoader(); if (loader != null) { ldtStart = getCurrentTimeMillis(); // build a list of the missing keys to load Set setRequest = new HashSet(colKeys); setRequest.removeAll(map.keySet()); // load the missing keys loadAll(setRequest); // whichever ones are now loaded, add their values to the // result map.putAll(peekAll(setRequest)); } } // update stats m_stats.registerHits(cHits, ldtStart); m_stats.registerMisses(cTotal - cHits, ldtStart); return map; } /** * Indicates to the cache that the specified key should be loaded into the * cache, if it is not already in the cache. This provides a means to * "pre-load" a single entry into the cache using the cache's loader. *

* If a valid entry with the specified key already exists in the cache, * or if the cache does not have a loader, then this method has no effect. *

* An implementation may perform the load operation asynchronously. * * @param oKey the key to request to be loaded */ public void load(final Object oKey) { CacheLoader loader = getCacheLoader(); if (loader != null && getEntryInternal(oKey) == null) { Object oValue = loader.load(oKey); if (oValue != null) { KeyMask mask = new KeyMask() { public boolean isIgnored(Object oCheckKey) { return equals(oKey, oCheckKey); } }; setKeyMask(mask); try { super.put(oKey, oValue); } finally { setKeyMask(null); } } } } /** * Indicates to the cache that it should load data from its loader to * fill the cache; this is sometimes referred to as "pre-loading" or * "warming" a cache. *

* The specific set of data that will be loaded is unspecified. The * implementation may choose to load all data, some specific subset * of the data, or no data. An implementation may require that the * loader implement the IterableCacheLoader interface in order for * this method to load any data. *

* An implementation may perform the load operation asynchronously. */ public void loadAll() { CacheLoader loader = getCacheLoader(); if (loader instanceof IterableCacheLoader) { Iterator iter = ((IterableCacheLoader) loader).keys(); int cMaxUnits = getHighUnits(); if (cMaxUnits > 0 && cMaxUnits < Integer.MAX_VALUE) { int cTarget = Math.max(getLowUnits(), (int) (0.9 * cMaxUnits)); int cCurrent = getUnits(); while (iter.hasNext() && cCurrent < cTarget) { load(iter.next()); int cUnits = getUnits(); if (cUnits < cCurrent) { // cache is already starting to prune itself for // some reason; assume that eviction occurred // which is an indication that we've warmed the // cache suitably break; } cCurrent = cUnits; } } else { loadAll(new ImmutableArrayList(SimpleEnumerator.toArray(iter))); } } } /** * Indicates to the cache that the specified keys should be loaded into * the cache, if they are not already in the cache. This provides a means * to "pre-load" entries into the cache using the cache's loader. *

* The result of this method is defined to be semantically the same as * the following implementation: * *


    * CacheLoader loader = getCacheLoader();
    * if (loader != null && !colKeys.isEmpty())
    *     {
    *     Set setRequest = new HashSet(colKeys);
    *     setRequest.removeAll(peekAll(colKeys).keySet());
    *     if (!setRequest.isEmpty())
    *         {
    *         Map map = loader.loadAll(colKeys);
    *         if (!map.isEmpty())
    *             {
    *             putAll(map);
    *             }
    *         }
    *     }
    * 
* * @param colKeys a collection of keys to request to be loaded */ public void loadAll(Collection colKeys) { CacheLoader loader = getCacheLoader(); if (loader != null && !colKeys.isEmpty()) { Set setRequest = new HashSet(colKeys); setRequest.removeAll(peekAll(colKeys).keySet()); if (!setRequest.isEmpty()) { Map map = loader.loadAll(setRequest); if (!map.isEmpty()) { final Set setKeys = map.keySet(); KeyMask mask = new KeyMask() { public boolean isIgnored(Object oCheckKey) { return setKeys.contains(oCheckKey); } }; setKeyMask(mask); try { super.putAll(map); } finally { setKeyMask(null); } } } } } /** * Checks for a valid entry corresponding to the specified key in the * cache, and returns the corresponding value if it is. If it is not in * the cache, returns null, and does not attempt to load the value using * its cache loader. * * @param oKey the key to "peek" into the cache for * * @return the value corresponding to the specified key */ public Object peek(Object oKey) { // avoid super.get() because it affects statistics LocalCache.Entry entry = (LocalCache.Entry) getEntryInternal(oKey); return entry == null ? null : entry.getValue(); } /** * Checks for a valid entry corresponding to each specified key in the * cache, and places the corresponding value in the returned map if it is. * For each key that is not in the cache, no entry is placed into the * returned map. The cache does not attempt to load any values using * its cache loader. *

* The result of this method is defined to be semantically the same as * the following implementation, without regards to threading issues: * *


    * Map map = new HashMap();
    * for (Iterator iter = colKeys.iterator(); iter.hasNext(); )
    *     {
    *     Object oKey   = iter.next();
    *     Object oValue = peek(oKey);
    *     if (oValue != null || containsKey(oKey))
    *         {
    *         map.put(oKey, oValue);
    *         }
    *     }
    * return map;
    * 
* * @param colKeys a collection of keys to "peek" into the cache for * * @return a Map of keys that were found in the cache and their values */ public Map peekAll(Collection colKeys) { Map map = new HashMap(colKeys.size()); for (Iterator iter = colKeys.iterator(); iter.hasNext(); ) { Object oKey = iter.next(); LocalCache.Entry entry = (LocalCache.Entry) getEntryInternal(oKey); if (entry != null) { map.put(oKey, entry.getValue()); } } return map; } // ----- inner class: EntrySet ------------------------------------------ /** * Factory pattern. * * @return a new instance of the EntrySet class (or a subclass thereof) */ protected SafeHashMap.EntrySet instantiateEntrySet() { return new LocalCache.EntrySet(); } /** * A set of entries backed by this map. */ protected class EntrySet extends SafeHashMap.EntrySet { // ----- Set interface ------------------------------------------ @Override public Iterator iterator() { // optimization if (LocalCache.super.isEmpty()) { return NullImplementation.getIterator(); } // complete entry set iterator Iterator iter = instantiateIterator(); // filter to get rid of expired objects Filter filter = (LocalCache.Entry entry) -> !removeIfExpired(entry); return new FilterEnumerator(iter, filter); } @Override public Object[] toArray(Object ao[]) { Object[] aoAll = super.toArray(ao); int cAll = aoAll.length; int ofSrc = 0; int ofDest = 0; while (ofSrc < cAll) { LocalCache.Entry entry = (LocalCache.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 LocalCache.KeySet(); } /** * A set of entries backed by this map. */ protected class KeySet extends SafeHashMap.KeySet { // ----- Set interface ------------------------------------------ @Override public Object[] toArray(Object ao[]) { // build list of non-expired keys Object[] aoAll; int cAll = 0; // synchronizing prevents add/remove, keeping size() constant LocalCache map = LocalCache.this; synchronized (map) { // create the array to store the map keys int c = 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 LocalCache.Entry entry = (LocalCache.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; } @Override public int size() { // COH-1089: get the size value without causing any eviction // (see SafeHashMap#size) return LocalCache.super.size(); } } // ----- inner class: ValuesCollection ---------------------------------- /** * Factory pattern. * * @return a new instance of the ValuesCollection class (or subclass * thereof) */ protected SafeHashMap.ValuesCollection instantiateValuesCollection() { return new LocalCache.ValuesCollection(); } /** * A collection of values backed by this map. */ protected class ValuesCollection extends SafeHashMap.ValuesCollection { // ----- Collection interface ----------------------------------- @Override public Object[] toArray(Object ao[]) { // build list of non-expired values Object[] aoAll; int cAll = 0; // synchronizing prevents add/remove, keeping size() constant LocalCache map = LocalCache.this; synchronized (map) { // create the array to store the map values int c = 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 LocalCache.Entry entry = (LocalCache.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; } @Override public int size() { // COH-1089: get the size value without causing any eviction // (see SafeHashMap#size) return LocalCache.super.size(); } } // ----- 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 --------------------------------------- /** * 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); } /** * 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 to 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 ----------------------------------------------------- /** * Returns the CacheStatistics for this cache. * * @return a CacheStatistics object */ public CacheStatistics getCacheStatistics() { return m_stats; } /** * 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 ------------------------------------------------------- /** * Attempt to call evict() when no one else is, to avoid contention on * opportunistic attempts at evicting. */ protected void tryEvict() { long lCurrent = getCurrentTimeMillis(); if (lCurrent > m_lNextFlush && m_apprvrEvict != ConfigurableCacheMap.EvictionApprover.DISAPPROVER && f_evictLock.getAndSet(false)) { try { // only one thread calling tryEvict() is going to contend inside evict() evict(); } finally { f_evictLock.lazySet(true); } } } /** * 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 LocalCache.InternalEvictionPolicy) { nType = ((LocalCache.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 == LocalCache.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(); ) { LocalCache.Entry entry = (LocalCache.Entry) iter.next(); int cUnits = entry.calculateUnits(entry.getValue()); // update both the entry unit count and total unit count entry.setUnits(cUnits); } } @Override protected SafeHashMap.Entry getEntryInternal(Object oKey) { LocalCache.Entry entry = (LocalCache.Entry) super.getEntryInternal(oKey); if (entry != null && removeIfExpired(entry)) { entry = null; } return entry; } /** * Determine the store used by this LocalCache, if any. * * @return the CacheStore used by this LocalCache, or null if none */ protected CacheStore getCacheStore() { return m_store; } /** * ThreadLocal: Get the current key mask for the current thread. * * @return the current key mask */ protected KeyMask getKeyMask() { KeyMask mask = (KeyMask) m_tloIgnore.get(); return mask == null ? DEFAULT_KEY_MASK : mask; } /** * ThreadLocal: Set the key mask for the current thread. * * @param mask the new key mask, or null to clear the mask */ protected void setKeyMask(KeyMask mask) { m_tloIgnore.set(mask); } /** * 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(LocalCache.Entry entry) { long dtExpiry = entry.getExpiryMillis(); boolean fExpired = dtExpiry != 0 && (dtExpiry & ~0xFFL) < getCurrentTimeMillis(); KeyMask mask = getKeyMask(); boolean fPrev = mask.ensureSynthetic(); boolean fPrevExpired = fExpired ? mask.ensureExpired() : false; try { ConfigurableCacheMap.EvictionApprover appr = m_apprvrEvict; if (appr == null || appr.isEvictable(entry)) { entry.discard(); removeEntryInternal(entry); return true; } else { return false; } } finally { mask.setSynthetic(fPrev); mask.setExpired(fPrevExpired); } } /** * Factory pattern: instantiate a new CacheEvent corresponding * to the specified parameters. * * @return a new instance of the CacheEvent class (or a subclass thereof) */ protected MapEvent instantiateMapEvent(int nId, Object oKey, Object oValueOld, Object oValueNew) { return new CacheEvent(this, nId, oKey, oValueOld, oValueNew, getKeyMask().isSynthetic(), CacheEvent.TransformationState.TRANSFORMABLE, false, getKeyMask().isExpired()); } /** * Remove an entry (if it is eligible for eviction) because it has expired. *

* Note: This method is the same as {@link #removeEvicted(LocalCache.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(LocalCache.Entry)} instead */ protected boolean removeExpired(LocalCache.Entry entry, boolean fRemoveInternal) { return removeEvicted(entry); } /** * Remove an entry if it has expired. * * @param entry the entry * * @return true iff the entry was actually removed */ protected boolean removeIfExpired(LocalCache.Entry entry) { if (entry.isExpired() && m_apprvrEvict != ConfigurableCacheMap.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 == ConfigurableCacheMap.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) { LocalCache.Entry entry = (LocalCache.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((LocalCache.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) { LocalCache.Entry entry = (LocalCache.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 LocalCache.Entry) { cRemEvict -= queueForEviction((LocalCache.Entry) o, listEvict); if (cRemEvict <= 0L) { break; } } else { List list = (List) o; for (Object entry : list) { cRemEvict -= queueForEviction((LocalCache.Entry) entry, listEvict); if (cRemEvict <= 0L) { break ForEachEntry; } } } } } break; } if (!fLRU) { // reset touch counts SafeHashMap.Entry[] aeBucket = m_aeBucket; for (SafeHashMap.Entry e : aeBucket) { LocalCache.Entry entry = (LocalCache.Entry) e; while (entry != null) { entry.resetTouchCount(); entry = entry.getNext(); } } } // store off the list of pending evictions if (listEvict != null) { m_iterEvict = listEvict.iterator(); } // 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, Set listEvict) private int queueForEviction(LocalCache.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() { Iterator 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()) { LocalCache.Entry entry = (LocalCache.Entry) getEntryInternal(iterEvict.next()); iterEvict.remove(); 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(); } // ----- 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 LocalCache has any listeners at all. * * @return true iff this LocalCache 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: InternalListener ---------------------------------- /** * Factory pattern: Instantiate an internal MapListener to listen to this * cache and report changes to the CacheStore. * * @return a new MapListener instance */ protected MapListener instantiateInternalListener() { return new InternalListener(); } /** * An internal MapListener that listens to this cache and reports * changes to the CacheStore. */ protected class InternalListener extends Base implements MapListener { /** * Invoked when a map entry has been inserted. * * @param evt the MapEvent carrying the insert information */ public void entryInserted(MapEvent evt) { onModify(evt); } /** * Invoked when a map entry has been updated. * * @param evt the MapEvent carrying the update information */ public void entryUpdated(MapEvent evt) { onModify(evt); } /** * Invoked when a map entry has been removed. * * @param evt the MapEvent carrying the delete information */ public void entryDeleted(MapEvent evt) { // deletions are handled by the clear() and remove(Object) // methods, and are ignored by the listener, because they // include evictions, which may be impossible to differentiate // from client-invoked removes and clears } /** * A value modification event (insert or update) has occurred. * * @param evt the MapEvent object */ protected void onModify(MapEvent evt) { if (!getKeyMask().isIgnored(evt.getKey())) { CacheStore store = getCacheStore(); if (store != null) { store.store(evt.getKey(), evt.getNewValue()); } } } } // ----- inner class: KeyMask ------------------------------------------- /** * A class that masks certain changes so that they are not reported back * to the CacheStore. */ protected class KeyMask extends Base { /** * Check if a key should be ignored. * * @param oKey the key that a change event has occurred for * * @return true if change events for the key should be ignored */ public boolean isIgnored(Object oKey) { return false; } /** * Check whether or not the currently performed operation is * internally initiated. * * @return true iff the current operation is internal */ public boolean isSynthetic() { return true; } /** * Specify whether or not the currently performed operation is internally * initiated. * * @param fSynthetic true iff the the current operation is internal */ public void setSynthetic(boolean fSynthetic) { } /** * Check whether or not the currently performed operation has been initiated * because the entry expired. * * @return true iff the entry has expired * * @since 22.06 */ public boolean isExpired() { return true; } /** * Specify whether or not the currently performed operation concerns an * expired entry * * @param fExpired true iff the current operation is an expiration one * * @since 22.06 */ public void setExpired(boolean fExpired) { } /** * Ensure that the synthetic operation flag is set. * * @return the previous value of the flag */ public boolean ensureSynthetic() { boolean f = isSynthetic(); if (!f) { setSynthetic(true); } return f; } /** * Ensure that the expired operation flag is set. * * @return the previous value of the flag * * @since 22.06 */ public boolean ensureExpired() { boolean f = isExpired(); if (!f) { setExpired(true); } return f; } } // ----- inner class: Entry --------------------------------------------- /** * Factory method. This method exists to allow the LocalCache 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 LocalCache.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(); } @Override protected void onAdd() { scheduleExpiry(); // update units int cNewUnits = calculateUnits(m_oValue); LocalCache map = LocalCache.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 ------------------------------------ @Override 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); LocalCache map = LocalCache.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 ------------------------------ @Override protected void copyFrom(SafeHashMap.Entry entry) { LocalCache.Entry entryThat = (LocalCache.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 LocalCache.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; } @Override public void touch() { ++m_cUses; m_dtLastUse = getCurrentTimeMillis(); ConfigurableCacheMap.EvictionPolicy policy = LocalCache.this.m_policy; if (policy != null) { policy.entryTouched(this); } } @Override public long getLastTouchMillis() { return m_dtLastUse; } @Override 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); } } @Override public long getExpiryMillis() { return m_dtExpiry; } @Override 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 synchronized void registerExpiry(long lMillis) { LongArray arrayExpiry = m_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 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 = LocalCache.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); } LocalCache map = LocalCache.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) { LocalCache map = LocalCache.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; } } @Override public int getUnits() { return m_cUnits; } @Override public void setUnits(int cUnits) { azzert(cUnits >= 0); synchronized (LocalCache.this) { int cOldUnits = m_cUnits; if (cOldUnits == -1) { // entry is discarded; avoid exception return; } if (cUnits != cOldUnits) { LocalCache.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 */ LocalCache.Entry getNext() { return (LocalCache.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(LocalCache.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; } // ----- 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; } @Override public void entryTouched(ConfigurableCacheMap.Entry entry) { throw new UnsupportedOperationException(); } @Override public void requestEviction(int cMaximum) { throw new UnsupportedOperationException(); } @Override 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 LocalCache eviction type represented by this * InternalEvictionPolicy. * * @return one of the EVICTION_POLICY_* constants */ public int getEvictionType() { return m_nType; } /** * The LocalCache eviction type represented by this * InternalEvictionPolicy; one of the EVICTION_POLICY_* constants. */ private int m_nType; } // ----- 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() { } @Override public int calculateUnits(Object oKey, Object oValue) { return 1; } @Override public String getName() { return "Internal-Fixed"; } /** * Singleton instance. */ public static final LocalCache.InternalUnitCalculator INSTANCE = new LocalCache.InternalUnitCalculator(); } // ----- constants ------------------------------------------------------ /** * By default, the cache size (in units) is infinite. */ public static final int DEFAULT_UNITS = Integer.MAX_VALUE; /** * By default, the cache entries never expire. */ public static final int DEFAULT_EXPIRE = 0; /** * The default key mask that ignores nothing. */ protected final KeyMask DEFAULT_KEY_MASK = new KeyMask() { public boolean isSynthetic() { return m_fSynthetic; } public void setSynthetic(boolean fSynthetic) { m_fSynthetic = fSynthetic; } public boolean isExpired() { return m_fExpired; } public void setExpired(boolean fExpired) { m_fExpired = fExpired; } private boolean m_fSynthetic = false; private boolean m_fExpired = false; }; /** * 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 LocalCache.EvictionPolicy INSTANCE_HYBRID = new LocalCache.InternalEvictionPolicy(EVICTION_POLICY_HYBRID); /** * The EvictionPolicy object for the Least Recently Used (LRU) eviction * algorithm. */ public static final LocalCache.EvictionPolicy INSTANCE_LRU = new LocalCache.InternalEvictionPolicy(EVICTION_POLICY_LRU); /** * The EvictionPolicy object for the Least Frequently Used (LFU) eviction * algorithm. */ public static final LocalCache.EvictionPolicy INSTANCE_LFU = new LocalCache.InternalEvictionPolicy(EVICTION_POLICY_LFU); /** * The UnitCalculator object that counts each entry as one unit. */ public static final LocalCache.UnitCalculator INSTANCE_FIXED = LocalCache.InternalUnitCalculator.INSTANCE; /** * The UnitCalculator object that measures the bytes used by entries. This * is intended for caches that manage binary data. */ public static final LocalCache.UnitCalculator INSTANCE_BINARY = BinaryMemoryCalculator.INSTANCE; // ----- data members --------------------------------------------------- /** * The loader used by this cache for misses. */ private CacheLoader m_loader; /** * The store used by this cache for modifications. If this value is * non-null, then it is the same reference as the loader. */ private CacheStore m_store; /** * The map listener used by this cache to listen to itself in order to * pass events to the CacheStore. Only used when there is a CacheStore. */ private MapListener m_listener; /** * The thread-local object to check for keys that the current thread * is supposed to ignore if those keys change. Contains KeyMask objects. */ private ThreadLocal m_tloIgnore = new ThreadLocal(); /** * 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; /** * A lock for access to {@link #m_listenerSupport}. */ private final Lock f_listenerLock = new ReentrantLock(); /** * 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 Iterator m_iterEvict; /** * Specifies whether or not this cache will incrementally evict. */ protected boolean m_fIncrementalEvict = true; /** * The EvictionApprover. */ protected volatile 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; /** * Lock for limiting concurrent eviction, allows for relaxing contention * when eviction is done opportunistically. */ private final AtomicBoolean f_evictLock = new AtomicBoolean(true); }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy