package org.apache.commons.jcs.engine.control;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.jcs.access.exception.CacheException;
import org.apache.commons.jcs.access.exception.ObjectNotFoundException;
import org.apache.commons.jcs.auxiliary.AuxiliaryCache;
import org.apache.commons.jcs.engine.CacheConstants;
import org.apache.commons.jcs.engine.CacheStatus;
import org.apache.commons.jcs.engine.behavior.ICache;
import org.apache.commons.jcs.engine.behavior.ICacheElement;
import org.apache.commons.jcs.engine.behavior.ICompositeCacheAttributes;
import org.apache.commons.jcs.engine.behavior.ICompositeCacheAttributes.DiskUsagePattern;
import org.apache.commons.jcs.engine.behavior.IElementAttributes;
import org.apache.commons.jcs.engine.behavior.IRequireScheduler;
import org.apache.commons.jcs.engine.control.event.ElementEvent;
import org.apache.commons.jcs.engine.control.event.behavior.ElementEventType;
import org.apache.commons.jcs.engine.control.event.behavior.IElementEvent;
import org.apache.commons.jcs.engine.control.event.behavior.IElementEventHandler;
import org.apache.commons.jcs.engine.control.event.behavior.IElementEventQueue;
import org.apache.commons.jcs.engine.control.group.GroupId;
import org.apache.commons.jcs.engine.match.KeyMatcherPatternImpl;
import org.apache.commons.jcs.engine.match.behavior.IKeyMatcher;
import org.apache.commons.jcs.engine.memory.behavior.IMemoryCache;
import org.apache.commons.jcs.engine.memory.lru.LRUMemoryCache;
import org.apache.commons.jcs.engine.memory.shrinking.ShrinkerThread;
import org.apache.commons.jcs.engine.stats.CacheStats;
import org.apache.commons.jcs.engine.stats.StatElement;
import org.apache.commons.jcs.engine.stats.behavior.ICacheStats;
import org.apache.commons.jcs.engine.stats.behavior.IStatElement;
import org.apache.commons.jcs.engine.stats.behavior.IStats;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* This is the primary hub for a single cache/region. It controls the flow of items through the
* cache. The auxiliary and memory caches are plugged in here.
* <p>
* This is the core of a JCS region. Hence, this simple class is the core of JCS.
*/
public class CompositeCache<K, V>
implements ICache<K, V>, IRequireScheduler
{
/** log instance */
private static final Log log = LogFactory.getLog( CompositeCache.class );
/**
* EventQueue for handling element events. Lazy initialized. One for each region. To be more efficient, the manager
* should pass a shared queue in.
*/
private IElementEventQueue elementEventQ;
/** Auxiliary caches. */
@SuppressWarnings("unchecked") // OK because this is an empty array
private AuxiliaryCache<K, V>[] auxCaches = new AuxiliaryCache[0];
/** is this alive? */
private AtomicBoolean alive;
/** Region Elemental Attributes, default. */
private IElementAttributes attr;
/** Cache Attributes, for hub and memory auxiliary. */
private ICompositeCacheAttributes cacheAttr;
/** How many times update was called. */
private AtomicInteger updateCount;
/** How many times remove was called. */
private AtomicInteger removeCount;
/** Memory cache hit count */
private AtomicInteger hitCountRam;
/** Auxiliary cache hit count (number of times found in ANY auxiliary) */
private AtomicInteger hitCountAux;
/** Count of misses where element was not found. */
private AtomicInteger missCountNotFound;
/** Count of misses where element was expired. */
private AtomicInteger missCountExpired;
/**
* The cache hub can only have one memory cache. This could be made more flexible in the future,
* but they are tied closely together. More than one doesn't make much sense.
*/
private IMemoryCache<K, V> memCache;
/** Key matcher used by the getMatching API */
private IKeyMatcher<K> keyMatcher = new KeyMatcherPatternImpl<K>();
private ScheduledFuture<?> future;
/**
* Constructor for the Cache object
* <p>
* @param cattr The cache attribute
* @param attr The default element attributes
*/
public CompositeCache( ICompositeCacheAttributes cattr, IElementAttributes attr )
{
this.attr = attr;
this.cacheAttr = cattr;
this.alive = new AtomicBoolean(true);
this.updateCount = new AtomicInteger(0);
this.removeCount = new AtomicInteger(0);
this.hitCountRam = new AtomicInteger(0);
this.hitCountAux = new AtomicInteger(0);
this.missCountNotFound = new AtomicInteger(0);
this.missCountExpired = new AtomicInteger(0);
createMemoryCache( cattr );
if ( log.isInfoEnabled() )
{
log.info( "Constructed cache with name [" + cacheAttr.getCacheName() + "] and cache attributes " + cattr );
}
}
/**
* Injector for Element event queue
*
* @param queue
*/
public void setElementEventQueue( IElementEventQueue queue )
{
this.elementEventQ = queue;
}
/**
* @see org.apache.commons.jcs.engine.behavior.IRequireScheduler#setScheduledExecutorService(java.util.concurrent.ScheduledExecutorService)
*/
@Override
public void setScheduledExecutorService(ScheduledExecutorService scheduledExecutor)
{
if ( cacheAttr.isUseMemoryShrinker() )
{
future = scheduledExecutor.scheduleAtFixedRate(
new ShrinkerThread<K, V>(this), 0, cacheAttr.getShrinkerIntervalSeconds(),
TimeUnit.SECONDS);
}
}
/**
* This sets the list of auxiliary caches for this region.
* <p>
* @param auxCaches
*/
public void setAuxCaches( AuxiliaryCache<K, V>[] auxCaches )
{
this.auxCaches = auxCaches;
}
/**
* Get the list of auxiliary caches for this region.
* <p>
* @return an array of auxiliary caches, may be empty, never null
*/
public AuxiliaryCache<K, V>[] getAuxCaches()
{
return this.auxCaches;
}
/**
* Standard update method.
* <p>
* @param ce
* @throws IOException
*/
@Override
public void update( ICacheElement<K, V> ce )
throws IOException
{
update( ce, false );
}
/**
* Standard update method.
* <p>
* @param ce
* @throws IOException
*/
public void localUpdate( ICacheElement<K, V> ce )
throws IOException
{
update( ce, true );
}
/**
* Put an item into the cache. If it is localOnly, then do no notify remote or lateral
* auxiliaries.
* <p>
* @param cacheElement the ICacheElement<K, V>
* @param localOnly Whether the operation should be restricted to local auxiliaries.
* @throws IOException
*/
protected void update( ICacheElement<K, V> cacheElement, boolean localOnly )
throws IOException
{
if ( cacheElement.getKey() instanceof String
&& cacheElement.getKey().toString().endsWith( CacheConstants.NAME_COMPONENT_DELIMITER ) )
{
throw new IllegalArgumentException( "key must not end with " + CacheConstants.NAME_COMPONENT_DELIMITER
+ " for a put operation" );
}
else if ( cacheElement.getKey() instanceof GroupId )
{
throw new IllegalArgumentException( "key cannot be a GroupId " + " for a put operation" );
}
if ( log.isDebugEnabled() )
{
log.debug( "Updating memory cache " + cacheElement.getKey() );
}
updateCount.incrementAndGet();
synchronized ( this )
{
memCache.update( cacheElement );
updateAuxiliaries( cacheElement, localOnly );
}
cacheElement.getElementAttributes().setLastAccessTimeNow();
}
/**
* This method is responsible for updating the auxiliaries if they are present. If it is local
* only, any lateral and remote auxiliaries will not be updated.
* <p>
* Before updating an auxiliary it checks to see if the element attributes permit the operation.
* <p>
* Disk auxiliaries are only updated if the disk cache is not merely used as a swap. If the disk
* cache is merely a swap, then items will only go to disk when they overflow from memory.
* <p>
* This is called by update( cacheElement, localOnly ) after it updates the memory cache.
* <p>
* This is protected to make it testable.
* <p>
* @param cacheElement
* @param localOnly
* @throws IOException
*/
protected void updateAuxiliaries( ICacheElement<K, V> cacheElement, boolean localOnly )
throws IOException
{
// UPDATE AUXILLIARY CACHES
// There are 3 types of auxiliary caches: remote, lateral, and disk
// more can be added if future auxiliary caches don't fit the model
// You could run a database cache as either a remote or a local disk.
// The types would describe the purpose.
if ( log.isDebugEnabled() )
{
if ( auxCaches.length > 0 )
{
log.debug( "Updating auxiliary caches" );
}
else
{
log.debug( "No auxiliary cache to update" );
}
}
for ( ICache<K, V> aux : auxCaches )
{
if ( aux == null )
{
continue;
}
if ( log.isDebugEnabled() )
{
log.debug( "Auxiliary cache type: " + aux.getCacheType() );
}
switch (aux.getCacheType())
{
// SEND TO REMOTE STORE
case REMOTE_CACHE:
if ( log.isDebugEnabled() )
{
log.debug( "ce.getElementAttributes().getIsRemote() = "
+ cacheElement.getElementAttributes().getIsRemote() );
}
if ( cacheElement.getElementAttributes().getIsRemote() && !localOnly )
{
try
{
// need to make sure the group cache understands that
// the key is a group attribute on update
aux.update( cacheElement );
if ( log.isDebugEnabled() )
{
log.debug( "Updated remote store for " + cacheElement.getKey() + cacheElement );
}
}
catch ( IOException ex )
{
log.error( "Failure in updateExclude", ex );
}
}
break;
// SEND LATERALLY
case LATERAL_CACHE:
// lateral can't do the checking since it is dependent on the
// cache region restrictions
if ( log.isDebugEnabled() )
{
log.debug( "lateralcache in aux list: cattr " + cacheAttr.isUseLateral() );
}
if ( cacheAttr.isUseLateral() && cacheElement.getElementAttributes().getIsLateral() && !localOnly )
{
// DISTRIBUTE LATERALLY
// Currently always multicast even if the value is
// unchanged, to cause the cache item to move to the front.
aux.update( cacheElement );
if ( log.isDebugEnabled() )
{
log.debug( "updated lateral cache for " + cacheElement.getKey() );
}
}
break;
// update disk if the usage pattern permits
case DISK_CACHE:
if ( log.isDebugEnabled() )
{
log.debug( "diskcache in aux list: cattr " + cacheAttr.isUseDisk() );
}
if ( cacheAttr.isUseDisk()
&& cacheAttr.getDiskUsagePattern() == DiskUsagePattern.UPDATE
&& cacheElement.getElementAttributes().getIsSpool() )
{
aux.update( cacheElement );
if ( log.isDebugEnabled() )
{
log.debug( "updated disk cache for " + cacheElement.getKey() );
}
}
break;
default: // CACHE_HUB
break;
}
}
}
/**
* Writes the specified element to any disk auxiliaries. Might want to rename this "overflow" in
* case the hub wants to do something else.
* <p>
* If JCS is not configured to use the disk as a swap, that is if the the
* CompositeCacheAttribute diskUsagePattern is not SWAP_ONLY, then the item will not be spooled.
* <p>
* @param ce The CacheElement
*/
public void spoolToDisk( ICacheElement<K, V> ce )
{
// if the item is not spoolable, return
if ( !ce.getElementAttributes().getIsSpool() )
{
// there is an event defined for this.
handleElementEvent( ce, ElementEventType.SPOOLED_NOT_ALLOWED );
return;
}
boolean diskAvailable = false;
// SPOOL TO DISK.
for ( ICache<K, V> aux : auxCaches )
{
if ( aux != null && aux.getCacheType() == CacheType.DISK_CACHE )
{
diskAvailable = true;
if ( cacheAttr.getDiskUsagePattern() == DiskUsagePattern.SWAP )
{
// write the last items to disk.2
try
{
handleElementEvent( ce, ElementEventType.SPOOLED_DISK_AVAILABLE );
aux.update( ce );
}
catch ( IOException ex )
{
// impossible case.
log.error( "Problem spooling item to disk cache.", ex );
throw new IllegalStateException( ex.getMessage() );
}
if ( log.isDebugEnabled() )
{
log.debug( "spoolToDisk done for: " + ce.getKey() + " on disk cache[" + aux.getCacheName() + "]" );
}
}
else
{
if ( log.isDebugEnabled() )
{
log.debug( "DiskCache available, but JCS is not configured to use the DiskCache as a swap." );
}
}
}
}
if ( !diskAvailable )
{
try
{
handleElementEvent( ce, ElementEventType.SPOOLED_DISK_NOT_AVAILABLE );
}
catch ( Exception e )
{
log.error( "Trouble handling the ELEMENT_EVENT_SPOOLED_DISK_NOT_AVAILABLE element event", e );
}
}
}
/**
* Gets an item from the cache.
* <p>
* @param key
* @return element from the cache, or null if not present
* @see org.apache.commons.jcs.engine.behavior.ICache#get(Object)
*/
@Override
public ICacheElement<K, V> get( K key )
{
return get( key, false );
}
/**
* Do not try to go remote or laterally for this get.
* <p>
* @param key
* @return ICacheElement
*/
public ICacheElement<K, V> localGet( K key )
{
return get( key, true );
}
/**
* Look in memory, then disk, remote, or laterally for this item. The order is dependent on the
* order in the cache.ccf file.
* <p>
* Do not try to go remote or laterally for this get if it is localOnly. Otherwise try to go
* remote or lateral if such an auxiliary is configured for this region.
* <p>
* @param key
* @param localOnly
* @return ICacheElement
*/
protected ICacheElement<K, V> get( K key, boolean localOnly )
{
ICacheElement<K, V> element = null;
boolean found = false;
if ( log.isDebugEnabled() )
{
log.debug( "get: key = " + key + ", localOnly = " + localOnly );
}
synchronized (this)
{
try
{
// First look in memory cache
element = memCache.get( key );
if ( element != null )
{
// Found in memory cache
if ( isExpired( element ) )
{
if ( log.isDebugEnabled() )
{
log.debug( cacheAttr.getCacheName() + " - Memory cache hit, but element expired" );
}
missCountExpired.incrementAndGet();
remove( key );
element = null;
}
else
{
if ( log.isDebugEnabled() )
{
log.debug( cacheAttr.getCacheName() + " - Memory cache hit" );
}
// Update counters
hitCountRam.incrementAndGet();
}
found = true;
}
else
{
// Item not found in memory. If local invocation look in aux
// caches, even if not local look in disk auxiliaries
for (AuxiliaryCache<K, V> aux : auxCaches)
{
if ( aux != null )
{
CacheType cacheType = aux.getCacheType();
if ( !localOnly || cacheType == CacheType.DISK_CACHE )
{
if ( log.isDebugEnabled() )
{
log.debug( "Attempting to get from aux [" + aux.getCacheName() + "] which is of type: "
+ cacheType );
}
try
{
element = aux.get( key );
}
catch ( IOException e )
{
log.error( "Error getting from aux", e );
}
}
if ( log.isDebugEnabled() )
{
log.debug( "Got CacheElement: " + element );
}
// Item found in one of the auxiliary caches.
if ( element != null )
{
if ( isExpired( element ) )
{
if ( log.isDebugEnabled() )
{
log.debug( cacheAttr.getCacheName() + " - Aux cache[" + aux.getCacheName() + "] hit, but element expired." );
}
missCountExpired.incrementAndGet();
// This will tell the remotes to remove the item
// based on the element's expiration policy. The elements attributes
// associated with the item when it created govern its behavior
// everywhere.
remove( key );
element = null;
}
else
{
if ( log.isDebugEnabled() )
{
log.debug( cacheAttr.getCacheName() + " - Aux cache[" + aux.getCacheName() + "] hit" );
}
// Update counters
hitCountAux.incrementAndGet();
copyAuxiliaryRetrievedItemToMemory( element );
}
found = true;
break;
}
}
}
}
}
catch ( IOException e )
{
log.error( "Problem encountered getting element.", e );
}
}
if ( !found )
{
missCountNotFound.incrementAndGet();
if ( log.isDebugEnabled() )
{
log.debug( cacheAttr.getCacheName() + " - Miss" );
}
}
if (element != null)
{
element.getElementAttributes().setLastAccessTimeNow();
}
return element;
}
/**
* Gets multiple items from the cache based on the given set of keys.
* <p>
* @param keys
* @return a map of K key to ICacheElement<K, V> element, or an empty map if there is no
* data in cache for any of these keys
*/
@Override
public Map<K, ICacheElement<K, V>> getMultiple( Set<K> keys )
{
return getMultiple( keys, false );
}
/**
* Gets multiple items from the cache based on the given set of keys. Do not try to go remote or
* laterally for this data.
* <p>
* @param keys
* @return a map of K key to ICacheElement<K, V> element, or an empty map if there is no
* data in cache for any of these keys
*/
public Map<K, ICacheElement<K, V>> localGetMultiple( Set<K> keys )
{
return getMultiple( keys, true );
}
/**
* Look in memory, then disk, remote, or laterally for these items. The order is dependent on
* the order in the cache.ccf file. Keep looking in each cache location until either the element
* is found, or the method runs out of places to look.
* <p>
* Do not try to go remote or laterally for this get if it is localOnly. Otherwise try to go
* remote or lateral if such an auxiliary is configured for this region.
* <p>
* @param keys
* @param localOnly
* @return ICacheElement
*/
protected Map<K, ICacheElement<K, V>> getMultiple( Set<K> keys, boolean localOnly )
{
Map<K, ICacheElement<K, V>> elements = new HashMap<K, ICacheElement<K, V>>();
if ( log.isDebugEnabled() )
{
log.debug( "get: key = " + keys + ", localOnly = " + localOnly );
}
try
{
// First look in memory cache
elements.putAll( getMultipleFromMemory( keys ) );
// If fewer than all items were found in memory, then keep looking.
if ( elements.size() != keys.size() )
{
Set<K> remainingKeys = pruneKeysFound( keys, elements );
elements.putAll( getMultipleFromAuxiliaryCaches( remainingKeys, localOnly ) );
}
}
catch ( IOException e )
{
log.error( "Problem encountered getting elements.", e );
}
// if we didn't find all the elements, increment the miss count by the number of elements not found
if ( elements.size() != keys.size() )
{
missCountNotFound.addAndGet(keys.size() - elements.size());
if ( log.isDebugEnabled() )
{
log.debug( cacheAttr.getCacheName() + " - " + ( keys.size() - elements.size() ) + " Misses" );
}
}
return elements;
}
/**
* Gets items for the keys in the set. Returns a map: key -> result.
* <p>
* @param keys
* @return the elements found in the memory cache
* @throws IOException
*/
private Map<K, ICacheElement<K, V>> getMultipleFromMemory( Set<K> keys )
throws IOException
{
Map<K, ICacheElement<K, V>> elementsFromMemory = memCache.getMultiple( keys );
Iterator<ICacheElement<K, V>> elementFromMemoryIterator = new HashMap<K, ICacheElement<K, V>>( elementsFromMemory ).values().iterator();
while ( elementFromMemoryIterator.hasNext() )
{
ICacheElement<K, V> element = elementFromMemoryIterator.next();
if ( element != null )
{
// Found in memory cache
if ( isExpired( element ) )
{
if ( log.isDebugEnabled() )
{
log.debug( cacheAttr.getCacheName() + " - Memory cache hit, but element expired" );
}
missCountExpired.incrementAndGet();
remove( element.getKey() );
elementsFromMemory.remove( element.getKey() );
}
else
{
if ( log.isDebugEnabled() )
{
log.debug( cacheAttr.getCacheName() + " - Memory cache hit" );
}
// Update counters
hitCountRam.incrementAndGet();
}
}
}
return elementsFromMemory;
}
/**
* If local invocation look in aux caches, even if not local look in disk auxiliaries.
* <p>
* @param keys
* @param localOnly
* @return the elements found in the auxiliary caches
* @throws IOException
*/
private Map<K, ICacheElement<K, V>> getMultipleFromAuxiliaryCaches( Set<K> keys, boolean localOnly )
throws IOException
{
Map<K, ICacheElement<K, V>> elements = new HashMap<K, ICacheElement<K, V>>();
Set<K> remainingKeys = new HashSet<K>( keys );
for ( AuxiliaryCache<K, V> aux : auxCaches )
{
if ( aux != null )
{
Map<K, ICacheElement<K, V>> elementsFromAuxiliary =
new HashMap<K, ICacheElement<K, V>>();
CacheType cacheType = aux.getCacheType();
if ( !localOnly || cacheType == CacheType.DISK_CACHE )
{
if ( log.isDebugEnabled() )
{
log.debug( "Attempting to get from aux [" + aux.getCacheName() + "] which is of type: "
+ cacheType );
}
try
{
elementsFromAuxiliary.putAll( aux.getMultiple( remainingKeys ) );
}
catch ( IOException e )
{
log.error( "Error getting from aux", e );
}
}
if ( log.isDebugEnabled() )
{
log.debug( "Got CacheElements: " + elementsFromAuxiliary );
}
processRetrievedElements( aux, elementsFromAuxiliary );
elements.putAll( elementsFromAuxiliary );
if ( elements.size() == keys.size() )
{
break;
}
else
{
remainingKeys = pruneKeysFound( keys, elements );
}
}
}
return elements;
}
/**
* Build a map of all the matching elements in all of the auxiliaries and memory.
* <p>
* @param pattern
* @return a map of K key to ICacheElement<K, V> element, or an empty map if there is no
* data in cache for any matching keys
*/
@Override
public Map<K, ICacheElement<K, V>> getMatching( String pattern )
{
return getMatching( pattern, false );
}
/**
* Build a map of all the matching elements in all of the auxiliaries and memory. Do not try to
* go remote or laterally for this data.
* <p>
* @param pattern
* @return a map of K key to ICacheElement<K, V> element, or an empty map if there is no
* data in cache for any matching keys
*/
public Map<K, ICacheElement<K, V>> localGetMatching( String pattern )
{
return getMatching( pattern, true );
}
/**
* Build a map of all the matching elements in all of the auxiliaries and memory. Items in
* memory will replace from the auxiliaries in the returned map. The auxiliaries are accessed in
* opposite order. It's assumed that those closer to home are better.
* <p>
* Do not try to go remote or laterally for this get if it is localOnly. Otherwise try to go
* remote or lateral if such an auxiliary is configured for this region.
* <p>
* @param pattern
* @param localOnly
* @return a map of K key to ICacheElement<K, V> element, or an empty map if there is no
* data in cache for any matching keys
*/
protected Map<K, ICacheElement<K, V>> getMatching( String pattern, boolean localOnly )
{
Map<K, ICacheElement<K, V>> elements = new HashMap<K, ICacheElement<K, V>>();
if ( log.isDebugEnabled() )
{
log.debug( "get: pattern [" + pattern + "], localOnly = " + localOnly );
}
try
{
// First look in auxiliaries
elements.putAll( getMatchingFromAuxiliaryCaches( pattern, localOnly ) );
// then look in memory, override aux with newer memory items.
elements.putAll( getMatchingFromMemory( pattern ) );
}
catch ( Exception e )
{
log.error( "Problem encountered getting elements.", e );
}
return elements;
}
/**
* Gets the key array from the memcache. Builds a set of matches. Calls getMultiple with the
* set. Returns a map: key -> result.
* <p>
* @param pattern
* @return a map of K key to ICacheElement<K, V> element, or an empty map if there is no
* data in cache for any matching keys
* @throws IOException
*/
protected Map<K, ICacheElement<K, V>> getMatchingFromMemory( String pattern )
throws IOException
{
// find matches in key array
// this avoids locking the memory cache, but it uses more memory
Set<K> keyArray = memCache.getKeySet();
Set<K> matchingKeys = getKeyMatcher().getMatchingKeysFromArray( pattern, keyArray );
// call get multiple
return getMultipleFromMemory( matchingKeys );
}
/**
* If local invocation look in aux caches, even if not local look in disk auxiliaries.
* <p>
* Moves in reverse order of definition. This will allow you to override those that are from the
* remote with those on disk.
* <p>
* @param pattern
* @param localOnly
* @return a map of K key to ICacheElement<K, V> element, or an empty map if there is no
* data in cache for any matching keys
* @throws IOException
*/
private Map<K, ICacheElement<K, V>> getMatchingFromAuxiliaryCaches( String pattern, boolean localOnly )
throws IOException
{
Map<K, ICacheElement<K, V>> elements = new HashMap<K, ICacheElement<K, V>>();
for ( int i = auxCaches.length - 1; i >= 0; i-- )
{
AuxiliaryCache<K, V> aux = auxCaches[i];
if ( aux != null )
{
Map<K, ICacheElement<K, V>> elementsFromAuxiliary =
new HashMap<K, ICacheElement<K, V>>();
CacheType cacheType = aux.getCacheType();
if ( !localOnly || cacheType == CacheType.DISK_CACHE )
{
if ( log.isDebugEnabled() )
{
log.debug( "Attempting to get from aux [" + aux.getCacheName() + "] which is of type: "
+ cacheType );
}
try
{
elementsFromAuxiliary.putAll( aux.getMatching( pattern ) );
}
catch ( IOException e )
{
log.error( "Error getting from aux", e );
}
if ( log.isDebugEnabled() )
{
log.debug( "Got CacheElements: " + elementsFromAuxiliary );
}
processRetrievedElements( aux, elementsFromAuxiliary );
elements.putAll( elementsFromAuxiliary );
}
}
}
return elements;
}
/**
* Remove expired elements retrieved from an auxiliary. Update memory with good items.
* <p>
* @param aux the auxiliary cache instance
* @param elementsFromAuxiliary
* @throws IOException
*/
private void processRetrievedElements( AuxiliaryCache<K, V> aux, Map<K, ICacheElement<K, V>> elementsFromAuxiliary )
throws IOException
{
Iterator<ICacheElement<K, V>> elementFromAuxiliaryIterator = new HashMap<K, ICacheElement<K, V>>( elementsFromAuxiliary ).values().iterator();
while ( elementFromAuxiliaryIterator.hasNext() )
{
ICacheElement<K, V> element = elementFromAuxiliaryIterator.next();
// Item found in one of the auxiliary caches.
if ( element != null )
{
if ( isExpired( element ) )
{
if ( log.isDebugEnabled() )
{
log.debug( cacheAttr.getCacheName() + " - Aux cache[" + aux.getCacheName() + "] hit, but element expired." );
}
missCountExpired.incrementAndGet();
// This will tell the remote caches to remove the item
// based on the element's expiration policy. The elements attributes
// associated with the item when it created govern its behavior
// everywhere.
remove( element.getKey() );
elementsFromAuxiliary.remove( element.getKey() );
}
else
{
if ( log.isDebugEnabled() )
{
log.debug( cacheAttr.getCacheName() + " - Aux cache[" + aux.getCacheName() + "] hit" );
}
// Update counters
hitCountAux.incrementAndGet();
copyAuxiliaryRetrievedItemToMemory( element );
}
}
}
}
/**
* Copies the item to memory if the memory size is greater than 0. Only spool if the memory
* cache size is greater than 0, else the item will immediately get put into purgatory.
* <p>
* @param element
* @throws IOException
*/
private void copyAuxiliaryRetrievedItemToMemory( ICacheElement<K, V> element )
throws IOException
{
if ( memCache.getCacheAttributes().getMaxObjects() > 0 )
{
memCache.update( element );
}
else
{
if ( log.isDebugEnabled() )
{
log.debug( "Skipping memory update since no items are allowed in memory" );
}
}
}
/**
* Returns a set of keys that were not found.
* <p>
* @param keys
* @param foundElements
* @return the original set of cache keys, minus any cache keys present in the map keys of the
* foundElements map
*/
private Set<K> pruneKeysFound( Set<K> keys, Map<K, ICacheElement<K, V>> foundElements )
{
Set<K> remainingKeys = new HashSet<K>( keys );
for (K key : foundElements.keySet())
{
remainingKeys.remove( key );
}
return remainingKeys;
}
/**
* Get a set of the keys for all elements in the cache
* <p>
* @return A set of the key type
*/
public Set<K> getKeySet()
{
return getKeySet(false);
}
/**
* Get a set of the keys for all elements in the cache
* <p>
* @param localOnly true if only memory keys are requested
*
* @return A set of the key type
*/
public Set<K> getKeySet(boolean localOnly)
{
HashSet<K> allKeys = new HashSet<K>();
allKeys.addAll( memCache.getKeySet() );
for ( AuxiliaryCache<K, V> aux : auxCaches )
{
if ( aux != null )
{
if(!localOnly || aux.getCacheType() == CacheType.DISK_CACHE)
{
try
{
allKeys.addAll( aux.getKeySet() );
}
catch ( IOException e )
{
// ignore
}
}
}
}
return allKeys;
}
/**
* Removes an item from the cache.
* <p>
* @param key
* @return true is it was removed
* @see org.apache.commons.jcs.engine.behavior.ICache#remove(Object)
*/
@Override
public boolean remove( K key )
{
return remove( key, false );
}
/**
* Do not propagate removeall laterally or remotely.
* <p>
* @param key
* @return true if the item was already in the cache.
*/
public boolean localRemove( K key )
{
return remove( key, true );
}
/**
* fromRemote: If a remove call was made on a cache with both, then the remote should have been
* called. If it wasn't then the remote is down. we'll assume it is down for all. If it did come
* from the remote then the cache is remotely configured and lateral removal is unnecessary. If
* it came laterally then lateral removal is unnecessary. Does this assume that there is only
* one lateral and remote for the cache? Not really, the initial removal should take care of the
* problem if the source cache was similarly configured. Otherwise the remote cache, if it had
* no laterals, would remove all the elements from remotely configured caches, but if those
* caches had some other weird laterals that were not remotely configured, only laterally
* propagated then they would go out of synch. The same could happen for multiple remotes. If
* this looks necessary we will need to build in an identifier to specify the source of a
* removal.
* <p>
* @param key
* @param localOnly
* @return true if the item was in the cache, else false
*/
protected boolean remove( K key, boolean localOnly )
{
removeCount.incrementAndGet();
boolean removed = false;
synchronized (this)
{
try
{
removed = memCache.remove( key );
}
catch ( IOException e )
{
log.error( e );
}
// Removes from all auxiliary caches.
for ( ICache<K, V> aux : auxCaches )
{
if ( aux == null )
{
continue;
}
CacheType cacheType = aux.getCacheType();
// for now let laterals call remote remove but not vice versa
if ( localOnly && ( cacheType == CacheType.REMOTE_CACHE || cacheType == CacheType.LATERAL_CACHE ) )
{
continue;
}
try
{
if ( log.isDebugEnabled() )
{
log.debug( "Removing " + key + " from cacheType" + cacheType );
}
boolean b = aux.remove( key );
// Don't take the remote removal into account.
if ( !removed && cacheType != CacheType.REMOTE_CACHE )
{
removed = b;
}
}
catch ( IOException ex )
{
log.error( "Failure removing from aux", ex );
}
}
}
return removed;
}
/**
* Clears the region. This command will be sent to all auxiliaries. Some auxiliaries, such as
* the JDBC disk cache, can be configured to not honor removeAll requests.
* <p>
* @see org.apache.commons.jcs.engine.behavior.ICache#removeAll()
*/
@Override
public void removeAll()
throws IOException
{
removeAll( false );
}
/**
* Will not pass the remove message remotely.
* <p>
* @throws IOException
*/
public void localRemoveAll()
throws IOException
{
removeAll( true );
}
/**
* Removes all cached items.
* <p>
* @param localOnly must pass in false to get remote and lateral aux's updated. This prevents
* looping.
* @throws IOException
*/
protected void removeAll( boolean localOnly )
throws IOException
{
synchronized (this)
{
try
{
memCache.removeAll();
if ( log.isDebugEnabled() )
{
log.debug( "Removed All keys from the memory cache." );
}
}
catch ( IOException ex )
{
log.error( "Trouble updating memory cache.", ex );
}
// Removes from all auxiliary disk caches.
for ( ICache<K, V> aux : auxCaches )
{
if ( aux != null && ( aux.getCacheType() == CacheType.DISK_CACHE || !localOnly ) )
{
try
{
if ( log.isDebugEnabled() )
{
log.debug( "Removing All keys from cacheType" + aux.getCacheType() );
}
aux.removeAll();
}
catch ( IOException ex )
{
log.error( "Failure removing all from aux", ex );
}
}
}
}
}
/**
* Flushes all cache items from memory to auxiliary caches and close the auxiliary caches.
*/
@Override
public void dispose()
{
dispose( false );
}
/**
* Invoked only by CacheManager. This method disposes of the auxiliaries one by one. For the
* disk cache, the items in memory are freed, meaning that they will be sent through the
* overflow channel to disk. After the auxiliaries are disposed, the memory cache is disposed.
* <p>
* @param fromRemote
*/
public void dispose( boolean fromRemote )
{
if ( log.isInfoEnabled() )
{
log.info( "In DISPOSE, [" + this.cacheAttr.getCacheName() + "] fromRemote [" + fromRemote + "]" );
}
// If already disposed, return immediately
if ( alive.compareAndSet(true, false) == false )
{
return;
}
synchronized (this)
{
// Try to stop shrinker thread
if (future != null)
{
future.cancel(true);
}
// Now, shut down the event queue
if (elementEventQ != null)
{
elementEventQ.dispose();
elementEventQ = null;
}
// Dispose of each auxiliary cache, Remote auxiliaries will be
// skipped if 'fromRemote' is true.
for ( ICache<K, V> aux : auxCaches )
{
try
{
// Skip this auxiliary if:
// - The auxiliary is null
// - The auxiliary is not alive
// - The auxiliary is remote and the invocation was remote
if ( aux == null || aux.getStatus() != CacheStatus.ALIVE
|| ( fromRemote && aux.getCacheType() == CacheType.REMOTE_CACHE ) )
{
if ( log.isInfoEnabled() )
{
log.info( "In DISPOSE, [" + this.cacheAttr.getCacheName() + "] SKIPPING auxiliary [" + aux.getCacheName() + "] fromRemote ["
+ fromRemote + "]" );
}
continue;
}
if ( log.isInfoEnabled() )
{
log.info( "In DISPOSE, [" + this.cacheAttr.getCacheName() + "] auxiliary [" + aux.getCacheName() + "]" );
}
// IT USED TO BE THE CASE THAT (If the auxiliary is not a lateral, or the cache
// attributes
// have 'getUseLateral' set, all the elements currently in
// memory are written to the lateral before disposing)
// I changed this. It was excessive. Only the disk cache needs the items, since only
// the disk cache is in a situation to not get items on a put.
if ( aux.getCacheType() == CacheType.DISK_CACHE )
{
int numToFree = memCache.getSize();
memCache.freeElements( numToFree );
if ( log.isInfoEnabled() )
{
log.info( "In DISPOSE, [" + this.cacheAttr.getCacheName() + "] put " + numToFree + " into auxiliary " + aux.getCacheName() );
}
}
// Dispose of the auxiliary
aux.dispose();
}
catch ( IOException ex )
{
log.error( "Failure disposing of aux.", ex );
}
}
if ( log.isInfoEnabled() )
{
log.info( "In DISPOSE, [" + this.cacheAttr.getCacheName() + "] disposing of memory cache." );
}
try
{
memCache.dispose();
}
catch ( IOException ex )
{
log.error( "Failure disposing of memCache", ex );
}
}
}
/**
* Calling save cause the entire contents of the memory cache to be flushed to all auxiliaries.
* Though this put is extremely fast, this could bog the cache and should be avoided. The
* dispose method should call a version of this. Good for testing.
*/
public void save()
{
if ( alive.compareAndSet(true, false) == false )
{
return;
}
synchronized ( this )
{
for ( ICache<K, V> aux : auxCaches )
{
try
{
if ( aux.getStatus() == CacheStatus.ALIVE )
{
for (K key : memCache.getKeySet())
{
ICacheElement<K, V> ce = memCache.get(key);
if (ce != null)
{
aux.update( ce );
}
}
}
}
catch ( IOException ex )
{
log.error( "Failure saving aux caches.", ex );
}
}
}
if ( log.isDebugEnabled() )
{
log.debug( "Called save for [" + cacheAttr.getCacheName() + "]" );
}
}
/**
* Gets the size attribute of the Cache object. This return the number of elements, not the byte
* size.
* <p>
* @return The size value
*/
@Override
public int getSize()
{
return memCache.getSize();
}
/**
* Gets the cacheType attribute of the Cache object.
* <p>
* @return The cacheType value
*/
@Override
public CacheType getCacheType()
{
return CacheType.CACHE_HUB;
}
/**
* Gets the status attribute of the Cache object.
* <p>
* @return The status value
*/
@Override
public CacheStatus getStatus()
{
return alive.get() ? CacheStatus.ALIVE : CacheStatus.DISPOSED;
}
/**
* Gets stats for debugging.
* <p>
* @return String
*/
@Override
public String getStats()
{
return getStatistics().toString();
}
/**
* This returns data gathered for this region and all the auxiliaries it currently uses.
* <p>
* @return Statistics and Info on the Region.
*/
public ICacheStats getStatistics()
{
ICacheStats stats = new CacheStats();
stats.setRegionName( this.getCacheName() );
// store the composite cache stats first
ArrayList<IStatElement<?>> elems = new ArrayList<IStatElement<?>>();
elems.add(new StatElement<Integer>( "HitCountRam", Integer.valueOf(getHitCountRam()) ) );
elems.add(new StatElement<Integer>( "HitCountAux", Integer.valueOf(getHitCountAux()) ) );
stats.setStatElements( elems );
// memory + aux, memory is not considered an auxiliary internally
int total = auxCaches.length + 1;
ArrayList<IStats> auxStats = new ArrayList<IStats>(total);
auxStats.add(getMemoryCache().getStatistics());
for ( AuxiliaryCache<K, V> aux : auxCaches )
{
auxStats.add(aux.getStatistics());
}
// store the auxiliary stats
stats.setAuxiliaryCacheStats( auxStats );
return stats;
}
/**
* Gets the cacheName attribute of the Cache object. This is also known as the region name.
* <p>
* @return The cacheName value
*/
@Override
public String getCacheName()
{
return cacheAttr.getCacheName();
}
/**
* Gets the default element attribute of the Cache object This returns a copy. It does not
* return a reference to the attributes.
* <p>
* @return The attributes value
*/
public IElementAttributes getElementAttributes()
{
if ( attr != null )
{
return attr.clone();
}
return null;
}
/**
* Sets the default element attribute of the Cache object.
* <p>
* @param attr
*/
public void setElementAttributes( IElementAttributes attr )
{
this.attr = attr;
}
/**
* Gets the ICompositeCacheAttributes attribute of the Cache object.
* <p>
* @return The ICompositeCacheAttributes value
*/
public ICompositeCacheAttributes getCacheAttributes()
{
return this.cacheAttr;
}
/**
* Sets the ICompositeCacheAttributes attribute of the Cache object.
* <p>
* @param cattr The new ICompositeCacheAttributes value
*/
public void setCacheAttributes( ICompositeCacheAttributes cattr )
{
this.cacheAttr = cattr;
// need a better way to do this, what if it is in error
this.memCache.initialize( this );
}
/**
* Gets the elementAttributes attribute of the Cache object.
* <p>
* @param key
* @return The elementAttributes value
* @throws CacheException
* @throws IOException
*/
public IElementAttributes getElementAttributes( K key )
throws CacheException, IOException
{
ICacheElement<K, V> ce = get( key );
if ( ce == null )
{
throw new ObjectNotFoundException( "key " + key + " is not found" );
}
return ce.getElementAttributes();
}
/**
* Determine if the element is expired based on the values of the element attributes
*
* @param element the element
*
* @return true if the element is expired
*/
public boolean isExpired( ICacheElement<K, V> element)
{
return isExpired(element, System.currentTimeMillis(),
ElementEventType.EXCEEDED_MAXLIFE_ONREQUEST,
ElementEventType.EXCEEDED_IDLETIME_ONREQUEST );
}
/**
* Check if the element is expired based on the values of the element attributes
*
* @param element the element
* @param timestamp the timestamp to compare to
* @param eventMaxlife the event to fire in case the max life time is exceeded
* @param eventIdle the event to fire in case the idle time is exceeded
*
* @return true if the element is expired
*/
public boolean isExpired(ICacheElement<K, V> element, long timestamp,
ElementEventType eventMaxlife, ElementEventType eventIdle)
{
try
{
IElementAttributes attributes = element.getElementAttributes();
if ( !attributes.getIsEternal() )
{
// Remove if maxLifeSeconds exceeded
long maxLifeSeconds = attributes.getMaxLife();
long createTime = attributes.getCreateTime();
final long timeFactorForMilliseconds = attributes.getTimeFactorForMilliseconds();
if ( maxLifeSeconds != -1 && ( timestamp - createTime ) > ( maxLifeSeconds * timeFactorForMilliseconds) )
{
if ( log.isDebugEnabled() )
{
log.debug( "Exceeded maxLife: " + element.getKey() );
}
handleElementEvent( element, eventMaxlife );
return true;
}
long idleTime = attributes.getIdleTime();
long lastAccessTime = attributes.getLastAccessTime();
// Remove if maxIdleTime exceeded
// If you have a 0 size memory cache, then the last access will
// not get updated.
// you will need to set the idle time to -1.
if ( ( idleTime != -1 ) && ( timestamp - lastAccessTime ) > idleTime * timeFactorForMilliseconds )
{
if ( log.isDebugEnabled() )
{
log.debug( "Exceeded maxIdle: " + element.getKey() );
}
handleElementEvent( element, eventIdle );
return true;
}
}
}
catch ( Exception e )
{
log.error( "Error determining expiration period, expiring", e );
return true;
}
return false;
}
/**
* If there are event handlers for the item, then create an event and queue it up.
* <p>
* This does not call handle directly; instead the handler and the event are put into a queue.
* This prevents the event handling from blocking normal cache operations.
* <p>
* @param element the item
* @param eventType the event type
*/
public void handleElementEvent( ICacheElement<K, V> element, ElementEventType eventType )
{
ArrayList<IElementEventHandler> eventHandlers = element.getElementAttributes().getElementEventHandlers();
if ( eventHandlers != null )
{
if ( log.isDebugEnabled() )
{
log.debug( "Element Handlers are registered. Create event type " + eventType );
}
if ( elementEventQ == null )
{
log.warn("No element event queue available for cache " + getCacheName());
return;
}
IElementEvent<ICacheElement<K, V>> event = new ElementEvent<ICacheElement<K, V>>( element, eventType );
for (IElementEventHandler hand : eventHandlers)
{
try
{
elementEventQ.addElementEvent( hand, event );
}
catch ( IOException e )
{
log.error( "Trouble adding element event to queue", e );
}
}
}
}
/**
* Create the MemoryCache based on the config parameters.
* TODO: consider making this an auxiliary, despite its close tie to the CacheHub.
* TODO: might want to create a memory cache config file separate from that of the hub -- ICompositeCacheAttributes
* <p>
* @param cattr
*/
private void createMemoryCache( ICompositeCacheAttributes cattr )
{
if ( memCache == null )
{
try
{
Class<?> c = Class.forName( cattr.getMemoryCacheName() );
@SuppressWarnings("unchecked") // Need cast
IMemoryCache<K, V> newInstance = (IMemoryCache<K, V>) c.newInstance();
memCache = newInstance;
memCache.initialize( this );
}
catch ( Exception e )
{
log.warn( "Failed to init mem cache, using: LRUMemoryCache", e );
this.memCache = new LRUMemoryCache<K, V>();
this.memCache.initialize( this );
}
}
else
{
log.warn( "Refusing to create memory cache -- already exists." );
}
}
/**
* Access to the memory cache for instrumentation.
* <p>
* @return the MemoryCache implementation
*/
public IMemoryCache<K, V> getMemoryCache()
{
return memCache;
}
/**
* Number of times a requested item was found in the memory cache.
* <p>
* @return number of hits in memory
*/
public int getHitCountRam()
{
return hitCountRam.get();
}
/**
* Number of times a requested item was found in and auxiliary cache.
* @return number of auxiliary hits.
*/
public int getHitCountAux()
{
return hitCountAux.get();
}
/**
* Number of times a requested element was not found.
* @return number of misses.
*/
public int getMissCountNotFound()
{
return missCountNotFound.get();
}
/**
* Number of times a requested element was found but was expired.
* @return number of found but expired gets.
*/
public int getMissCountExpired()
{
return missCountExpired.get();
}
/**
* @return Returns the updateCount.
*/
public int getUpdateCount()
{
return updateCount.get();
}
/**
* Sets the key matcher used by get matching.
* <p>
* @param keyMatcher
*/
@Override
public void setKeyMatcher( IKeyMatcher<K> keyMatcher )
{
if ( keyMatcher != null )
{
this.keyMatcher = keyMatcher;
}
}
/**
* Returns the key matcher used by get matching.
* <p>
* @return keyMatcher
*/
public IKeyMatcher<K> getKeyMatcher()
{
return this.keyMatcher;
}
/**
* This returns the stats.
* <p>
* @return getStats()
*/
@Override
public String toString()
{
return getStats();
}
}