package org.apache.jcs.engine.memory.mru;
/*
* 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.io.Serializable;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.ListIterator;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.jcs.engine.CacheConstants;
import org.apache.jcs.engine.behavior.ICacheElement;
import org.apache.jcs.engine.control.CompositeCache;
import org.apache.jcs.engine.control.group.GroupAttrName;
import org.apache.jcs.engine.control.group.GroupId;
import org.apache.jcs.engine.memory.AbstractMemoryCache;
import org.apache.jcs.engine.stats.StatElement;
import org.apache.jcs.engine.stats.Stats;
import org.apache.jcs.engine.stats.behavior.IStatElement;
import org.apache.jcs.engine.stats.behavior.IStats;
/**
* A SLOW reference management system. The most recently used items move to the
* front of the list and get spooled to disk if the cache hub is configured to
* use a disk cache.
* <p>
* This class is mainly for testing the hub. It also shows that use of the
* Collection LinkedList is far slower than JCS' own double linked list.
*/
public class MRUMemoryCache
extends AbstractMemoryCache
{
private static final long serialVersionUID = 5013101678192336129L;
private final static Log log = LogFactory.getLog( MRUMemoryCache.class );
private int hitCnt = 0;
private int missCnt = 0;
private int putCnt = 0;
/**
* Object to lock on the Field
*/
private int[] lockMe = new int[0];
/**
* MRU list.
*/
private LinkedList mrulist = new LinkedList();
/**
* For post reflection creation initialization
* @param hub
*/
public synchronized void initialize( CompositeCache hub )
{
super.initialize( hub );
log.info( "Initialized MRUMemoryCache for " + cacheName );
}
/**
* Puts an item to the cache.
* @param ce
* @exception IOException
*/
public void update( ICacheElement ce )
throws IOException
{
putCnt++;
Serializable key = ce.getKey();
ce.getElementAttributes().setLastAccessTimeNow();
// need a more fine grained locking here
boolean replace = false;
if ( map.containsKey( key ) )
{
replace = true;
}
synchronized ( lockMe )
{
map.put( key, ce );
if ( replace )
{
// the slowest method I've ever seen
mrulist.remove( key );
}
mrulist.addFirst( key );
}
// save a microsecond on the second call.
int size = map.size();
// need to spool at a certain percentage synchronously
if ( size < this.cattr.getMaxObjects() )
{
return;
}
// SPOOL LAST -- need to make this a grouping in a queue
if ( log.isDebugEnabled() )
{
log.debug( "In RAM overflow" );
}
// write the last item to disk.
try
{
// PUSH more than 1 TO DISK TO MINIMIZE THE TYPICAL spool at each
// put.
int chunkSizeCorrected = Math.min( size, chunkSize );
if ( log.isDebugEnabled() )
{
log.debug( "update: About to spool to disk cache, map.size() = " + size
+ ", this.cattr.getMaxObjects() = " + this.cattr.getMaxObjects() + ", chunkSizeCorrected = "
+ chunkSizeCorrected );
}
// The spool will put them in a disk event queue, so there is no
// need to pre-queue the queuing. This would be a bit wasteful
// and wouldn't save much time in this synchronous call.
for ( int i = 0; i < chunkSizeCorrected; i++ )
{
// remove the last
spoolLastElement();
}
if ( log.isDebugEnabled() )
{
log.debug( "update: After spool, map.size() = " + size + ", this.cattr.getMaxObjects() = "
+ this.cattr.getMaxObjects() + ", chunkSizeCorrected = " + chunkSizeCorrected );
}
}
catch ( Exception ex )
{
// impossible case.
log.error( "Problem updating MRU.", ex );
throw new IllegalStateException( ex.getMessage() );
}
}
/**
* This removes the last elemement in the list.
* <p>
* @return ICacheElement if there was a last element, else null.
*/
protected ICacheElement spoolLastElement()
{
ICacheElement toSpool = null;
// need a more fine grained locking here
synchronized ( lockMe )
{
Serializable last = (Serializable) mrulist.removeLast();
if ( last != null )
{
toSpool = (ICacheElement) map.get( last );
map.remove( last );
}
}
// Might want to rename this "overflow" incase the hub
// wants to do something else.
if ( toSpool != null )
{
cache.spoolToDisk( toSpool );
}
return toSpool;
}
/**
* This instructs the memory cache to remove the <i>numberToFree</i>
* according to its eviction policy. For example, the LRUMemoryCache will
* remove the <i>numberToFree</i> least recently used items. These will be
* spooled to disk if a disk auxiliary is available.
* <p>
* @param numberToFree
* @return the number that were removed. if you ask to free 5, but there are
* only 3, you will get 3.
* @throws IOException
*/
public int freeElements( int numberToFree )
throws IOException
{
int freed = 0;
for ( ; freed < numberToFree; freed++ )
{
ICacheElement element = spoolLastElement();
if ( element == null )
{
break;
}
}
return freed;
}
/**
* Get an item from the cache without affecting its last access time or
* position.
* @return Element matching key if found, or null
* @param key
* Identifies item to find
* @exception IOException
*/
public ICacheElement getQuiet( Serializable key )
throws IOException
{
ICacheElement ce = null;
try
{
ce = (ICacheElement) map.get( key );
if ( ce != null )
{
if ( log.isDebugEnabled() )
{
log.debug( cacheName + ": MRUMemoryCache quiet hit for " + key );
}
}
else
{
log.debug( cacheName + ": MRUMemoryCache quiet miss for " + key );
}
}
catch ( Exception e )
{
log.error( "Problem getting quietly from MRU.", e );
}
return ce;
}
/**
* Gets an item out of the map. If it finds an item, it is removed from the
* list and then added to the first position in the linked list.
* @return
* @param key
* @exception IOException
*/
public ICacheElement get( Serializable key )
throws IOException
{
ICacheElement ce = null;
boolean found = false;
try
{
if ( log.isDebugEnabled() )
{
log.debug( "get> key=" + key );
log.debug( "get> key=" + key.toString() );
}
ce = (ICacheElement) map.get( key );
if ( log.isDebugEnabled() )
{
log.debug( "ce =" + ce );
}
if ( ce != null )
{
found = true;
ce.getElementAttributes().setLastAccessTimeNow();
hitCnt++;
if ( log.isDebugEnabled() )
{
log.debug( cacheName + " -- RAM-HIT for " + key );
}
}
}
catch ( Exception e )
{
log.error( "Problem getting element.", e );
}
try
{
if ( !found )
{
// Item not found in cache.
missCnt++;
if ( log.isDebugEnabled() )
{
log.debug( cacheName + " -- MISS for " + key );
}
return null;
}
}
catch ( Exception e )
{
log.error( "Error handling miss", e );
return null;
}
try
{
synchronized ( lockMe )
{
mrulist.remove( ce.getKey() );
mrulist.addFirst( ce.getKey() );
}
}
catch ( Exception e )
{
log.error( "Error making first", e );
return null;
}
return ce;
}
/**
* Removes an item from the cache.
* @return
* @param key
* @exception IOException
*/
public boolean remove( Serializable key )
throws IOException
{
if ( log.isDebugEnabled() )
{
log.debug( "remove> key=" + key );
}
boolean removed = false;
// handle partial removal
if ( key instanceof String && key.toString().endsWith( CacheConstants.NAME_COMPONENT_DELIMITER ) )
{
// remove all keys of the same name hierarchy.
synchronized ( lockMe )
{
for ( Iterator itr = map.entrySet().iterator(); itr.hasNext(); )
{
Map.Entry entry = (Map.Entry) itr.next();
Object k = entry.getKey();
if ( k instanceof String && k.toString().startsWith( key.toString() ) )
{
itr.remove();
Serializable keyR = (Serializable) entry.getKey();
// map.remove( keyR );
mrulist.remove( keyR );
removed = true;
}
}
}
}
else if ( key instanceof GroupId )
{
// remove all keys of the same name hierarchy.
synchronized ( lockMe )
{
for ( Iterator itr = map.entrySet().iterator(); itr.hasNext(); )
{
Map.Entry entry = (Map.Entry) itr.next();
Object k = entry.getKey();
if ( k instanceof GroupAttrName && ( (GroupAttrName) k ).groupId.equals( key ) )
{
itr.remove();
mrulist.remove( k );
removed = true;
}
}
}
}
else
{
// remove single item.
if ( map.containsKey( key ) )
{
synchronized ( lockMe )
{
map.remove( key );
mrulist.remove( key );
}
removed = true;
}
}
// end else not hierarchical removal
return removed;
}
/**
* Get an Array of the keys for all elements in the memory cache
* @return Object[]
*/
public Object[] getKeyArray()
{
// need to lock to map here?
synchronized ( lockMe )
{
return map.keySet().toArray();
}
}
/**
* Dump the cache map for debugging.
*/
public void dumpMap()
{
log.debug( "dumpingMap" );
for ( Iterator itr = map.entrySet().iterator(); itr.hasNext(); )
{
// for ( Iterator itr = memCache.getIterator(); itr.hasNext();) {
Map.Entry e = (Map.Entry) itr.next();
ICacheElement ce = (ICacheElement) e.getValue();
log.debug( "dumpMap> key=" + e.getKey() + ", val=" + ce.getVal() );
}
}
/**
* Dump the cache entries from first to list for debugging.
*/
public void dumpCacheEntries()
{
log.debug( "dumpingCacheEntries" );
ListIterator li = mrulist.listIterator();
while ( li.hasNext() )
{
Serializable key = (Serializable) li.next();
log.debug( "dumpCacheEntries> key=" + key + ", val=" + ( (ICacheElement) map.get( key ) ).getVal() );
}
}
/*
* (non-Javadoc)
* @see org.apache.jcs.engine.memory.MemoryCache#getStatistics()
*/
public IStats getStatistics()
{
IStats stats = new Stats();
stats.setTypeName( "MRU Memory Cache" );
ArrayList elems = new ArrayList();
IStatElement se = null;
se = new StatElement();
se.setName( "List Size" );
se.setData( "" + mrulist.size() );
elems.add( se );
se = new StatElement();
se.setName( "Map Size" );
se.setData( "" + map.size() );
elems.add( se );
se = new StatElement();
se.setName( "Put Count" );
se.setData( "" + putCnt );
elems.add( se );
se = new StatElement();
se.setName( "Hit Count" );
se.setData( "" + hitCnt );
elems.add( se );
se = new StatElement();
se.setName( "Miss Count" );
se.setData( "" + missCnt );
elems.add( se );
// get an array and put them in the Stats object
IStatElement[] ses = (IStatElement[]) elems.toArray( new StatElement[0] );
stats.setStatElements( ses );
return stats;
}
}