/*
* Copyright 2005 Gregory Block, Ralf Joachim
*
* Licensed 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.
*/
package org.castor.cache.hashbelt;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.castor.cache.AbstractBaseCache;
import org.castor.cache.CacheAcquireException;
import org.castor.cache.hashbelt.container.Container;
import org.castor.cache.hashbelt.container.MapContainer;
import org.castor.cache.hashbelt.reaper.AbstractReaper;
import org.castor.cache.hashbelt.reaper.NullReaper;
import org.castor.core.util.concurrent.ReadWriteLock;
import org.castor.core.util.concurrent.WriterPreferenceReadWriteLock;
/**
* An abstract, core implementation of the hashbelt functionality; individual
* implementations will differ on the underlying behavior.
* <p>
* A hashbelt has six important values which get set at initialization:
* <dl>
* <dt>containers</dt>
* <dd>The number of containers in the conveyor belt. For example: If a box
* will drop off of the conveyor belt every 30 seconds, and you want a cache
* that lasts for 5 minutes, you want 5 / 30 = 6 containers on the belt. Every
* 30 seconds, another, clean container goes on the front of the conveyor belt,
* and everything in the last belt gets discarded. If not specified 10 containers
* are used by default.
* <br/>
* For systems with fine granularity, you are free to use a large number of
* containers; but the system is most efficient when the user decides on a
* "sweet spot" determining both the number of containers to be managed on the
* whole and the optimal number of buckets in those containers for managing. This
* is ultimately a performance/accuracy tradeoff with the actual discard-from-cache
* time being further from the mark as the rotation time goes up. Also the number
* of objects discarded at once when capacity limit is reached depends upon the
* number of containers.</dd>
* <dt>capacity</dt>
* <dd>Maximum capacity of the whole cache. If there are, for example, ten
* containers on the belt and the capacity has been set to 1000, each container
* will hold a maximum of 1000/10 objects. Therefore if the capacity limit is
* reached and the last container gets droped from the belt there are up to 100
* objects discarted at once. By default the capacity is set to 0 which causes
* capacity limit to be ignored so the cache can hold an undefined number of
* objects.</dd>
* <dt>ttl</dt>
* <dd>The maximum time an object lifes in cache. If the are, for example, ten
* containers and ttl is set to 300 seconds (5 minutes), a new container will be
* put in front of the belt every 300/10 = 30 seconds while another is dropped at
* the end at the same time. Due to the granularity of 30 seconds, everything just
* until 5 minutes 30 seconds will also end up in this box. The default value for
* ttl is 60 seconds. If ttl is set to 0 which means that objects life in cache
* for unlimited time and may only discarded by a capacity limit.</dd>
* <dt>monitor</dt>
* <dd>The monitor intervall in minutes when hashbelt cache rports the current
* number of containers used and objects cached. If set to 0 (default) monitoring
* is disabled.</dd>
* <dt>container-class</dt>
* <dd>The implementation of <b>org.castor.cache.hashbelt.container.Container</b>
* interface to be used for all containers of the cache. Castor provides the following
* 3 implementations of the Container interface.<br/>
* org.castor.cache.hashbelt.container.FastIteratingContainer<br/>
* org.castor.cache.hashbelt.container.MapContainer<br/>
* org.castor.cache.hashbelt.container.WeakReferenceContainer<br/>
* If not specified the MapContainer will be used as default.</dd>
* <dt>reaper-class</dt>
* <dd>Specific reapers yield different behaviors. The GC reaper, the default,
* just dumps the contents to the garbage collector. However, custom
* implementations may want to actually do something when a bucket drops off the
* end; see the javadocs on other available reapers to find a reaper strategy
* that meets your behavior requirements. Apart of the default
* <b>org.castor.cache.hashbelt.reaper.NullReaper</b> we provide 3 abstract
* implementations of <b>org.castor.cahe.hashbelt.reaper.Reaper</b> interface:<br/>
* org.castor.cache.hashbelt.reaper.NotifyingReaper<br/>
* org.castor.cache.hashbelt.reaper.RefreshingReaper<br/>
* org.castor.cache.hashbelt.reaper.ReinsertingReaper<br/>
* to be extended by your custom implementation.</dd>
* </dl>
*
* @author <a href="mailto:gblock AT ctoforaday DOT com">Gregory Block</a>
* @author <a href="mailto:ralf DOT joachim AT syscon DOT eu">Ralf Joachim</a>
* @version $Revision$ $Date: 2006-04-25 16:09:10 -0600 (Tue, 25 Apr 2006) $
* @since 1.0
*/
public abstract class AbstractHashbelt extends AbstractBaseCache {
//--------------------------------------------------------------------------
/** The <a href="http://jakarta.apache.org/commons/logging/">Jakarta
* Commons Logging</a> instance used for all logging. */
private static final Log LOG = LogFactory.getLog(AbstractHashbelt.class);
/** Mapped initialization parameter <code>containers</code>. */
public static final String PARAM_CONTAINERS = "containers";
/** Mapped initialization parameter <code>container-class</code>. */
public static final String PARAM_CONTAINER_CLASS = "container-class";
/** Mapped initialization parameter <code>reaper-class</code>. */
public static final String PARAM_REAPER_CLASS = "reaper-class";
/** Mapped initialization parameter <code>capacity</code>. */
public static final String PARAM_CAPACITY = "capacity";
/** Mapped initialization parameter <code>ttl</code>. */
public static final String PARAM_TTL = "ttl";
/** Mapped initialization parameter <code>monitor</code>. */
public static final String PARAM_MONITOR = "monitor";
/** Default number of containers for cache. */
public static final int DEFAULT_CONTAINERS = 10;
/** Default container class. */
public static final Class<? extends Container> DEFAULT_CONTAINER_CLASS = MapContainer.class;
/** Default reaper class. */
public static final Class<? extends AbstractReaper> DEFAULT_REAPER_CLASS = NullReaper.class;
/** Default capacity of cache. */
public static final int DEFAULT_CAPACITY = 0;
/** Default ttl of cache in seconds. */
public static final int DEFAULT_TTL = 60;
/** Default monitor interval of cache in minutes. */
public static final int DEFAULT_MONITOR = 0;
/** Milliseconds per second. */
private static final long ONE_SECOND = 1000;
/** Milliseconds per minute. */
private static final long ONE_MINUTE = 60 * ONE_SECOND;
//--------------------------------------------------------------------------
/** ReadWriteLock to synchronize access to cache. */
private final ReadWriteLock _lock = new WriterPreferenceReadWriteLock();
/** The internal array of containers building the cache. */
private Container[] _cache = new Container[0];
/** The internal array of empty conatiners to be used for cache on demand. */
private Container[] _pool;
/** Number of containers currently available in pool. */
private int _poolCount;
/** Real capacity limit of this cache. If set to Integer.MAX_VALUE the capacity
* of the cache is not restricted. The capacity needs to be greater then twice
* the number of containers. */
private int _cacheCapacity;
/** Approximat number of entries in this cache. */
private int _cacheSize = 0;
/** Target number of containers for this cache. The real number of containers
* may vary between 0 and twice the target number. */
private int _containerTarget;
/** Real number of containers in the cache. */
private int _containerCount = 0;
/** Capacity limit of a container. */
private int _containerCapacity;
/** Real ttl of the entries in this cache. */
private int _ttl;
/** The reaper to pass all expired containers to. */
private AbstractReaper _reaper;
/** Real monitor interval. */
private int _monitor;
/** Timer to expire containers and the objects they contain after ttl. */
private Timer _expirationTimer;
/** Timer to monitor container count and cache size every monitor interval. */
private Timer _monitoringTimer;
//--------------------------------------------------------------------------
// operations for life-cycle management of cache
/**
* {@inheritDoc}
*/
@SuppressWarnings("unchecked")
public final void initialize(final Properties params)
throws CacheAcquireException {
super.initialize(params);
String param;
try {
param = params.getProperty(PARAM_CONTAINERS);
if (param != null) { _containerTarget = Integer.parseInt(param); }
if (_containerTarget <= 0) { _containerTarget = DEFAULT_CONTAINERS; }
} catch (NumberFormatException ex) {
_containerTarget = DEFAULT_CONTAINERS;
}
try {
Class<? extends Container> cls = DEFAULT_CONTAINER_CLASS;
param = params.getProperty(PARAM_CONTAINER_CLASS);
if ((param != null) && !"".equals(param)) {
cls = (Class<? extends Container>) Class.forName(param);
}
_poolCount = 2 * _containerTarget;
_pool = new Container[_poolCount];
for (int i = 0; i < _poolCount; i++) {
_pool[i] = cls.newInstance();
}
} catch (Exception ex) {
String msg = "Failed to instantiate hashbelt container.";
throw new CacheAcquireException(msg, ex);
}
try {
Class<? extends AbstractReaper> cls = DEFAULT_REAPER_CLASS;
param = params.getProperty(PARAM_REAPER_CLASS);
if ((param != null) && !"".equals(param)) {
cls = (Class<? extends AbstractReaper>) Class.forName(param);
}
_reaper = cls.newInstance();
_reaper.setCache(this);
} catch (Exception ex) {
String msg = "Failed to instantiate hashbelt reaper.";
throw new CacheAcquireException(msg, ex);
}
try {
param = params.getProperty(PARAM_CAPACITY);
if (param != null) { _cacheCapacity = Integer.parseInt(param); }
if (_cacheCapacity < 0) { _cacheCapacity = DEFAULT_CAPACITY; }
} catch (NumberFormatException ex) {
_cacheCapacity = DEFAULT_CAPACITY;
}
int minCapacity = 2 * _containerTarget;
if ((_cacheCapacity > 0) && (_cacheCapacity < minCapacity)) {
_cacheCapacity = minCapacity;
}
_containerCapacity = _cacheCapacity / _containerTarget;
try {
param = params.getProperty(PARAM_TTL);
if (param != null) { _ttl = Integer.parseInt(param); }
if (_ttl < 0) { _ttl = DEFAULT_TTL; }
} catch (NumberFormatException ex) {
_ttl = DEFAULT_TTL;
}
if (_ttl > 0) {
long periode = (_ttl * ONE_SECOND) / _containerTarget;
_expirationTimer = new Timer(true);
_expirationTimer.schedule(new ExpirationTask(this), periode, periode);
}
try {
param = params.getProperty(PARAM_MONITOR);
if (param != null) { _monitor = Integer.parseInt(param); }
if (_monitor < 0) { _monitor = DEFAULT_MONITOR; }
} catch (NumberFormatException ex) {
_monitor = DEFAULT_MONITOR;
}
if (_monitor > 0) {
long periode = _monitor * ONE_MINUTE;
_monitoringTimer = new Timer(true);
_monitoringTimer.schedule(new MonitoringTask(this), periode, periode);
}
}
/**
* {@inheritDoc}
*/
public final void close() {
if (_monitoringTimer != null) {
_monitoringTimer.cancel();
_monitoringTimer = null;
}
_monitor = 0;
if (_expirationTimer != null) {
_expirationTimer.cancel();
_expirationTimer = null;
}
_ttl = 0;
clear();
_containerCapacity = 0;
_cacheCapacity = 0;
_reaper = null;
_poolCount = 0;
_pool = null;
_containerTarget = 0;
super.close();
}
//--------------------------------------------------------------------------
// getters/setters for cache configuration
/**
* Get real capacity of this cache.
*
* @return Real capacity of this cache.
*/
public final int getCapacity() { return _cacheCapacity; }
/**
* Get real ttl of this cache.
*
* @return Real ttl of this cache.
*/
public final int getTTL() { return _ttl; }
//--------------------------------------------------------------------------
// query operations of map interface
/**
* {@inheritDoc}
*/
public final int size() {
try {
_lock.readLock().acquire();
int size = _cacheSize;
_lock.readLock().release();
return size;
} catch (InterruptedException ex) {
return 0;
}
}
/**
* {@inheritDoc}
*/
public final boolean isEmpty() { return (size() == 0); }
/**
* {@inheritDoc}
*/
public final boolean containsKey(final Object key) {
if (key == null) { throw new NullPointerException("key"); }
boolean found = false;
try {
_lock.readLock().acquire();
} catch (InterruptedException ex) {
return false;
}
try {
for (int i = 0; (i < _containerCount) && !found; i++) {
if (_cache[i].containsKey(key)) { found = true; }
}
} catch (RuntimeException ex) {
throw ex;
} finally {
_lock.readLock().release();
}
return found;
}
/**
* {@inheritDoc}
*/
public final boolean containsValue(final Object value) {
if (value == null) { throw new NullPointerException("value"); }
boolean found = false;
try {
_lock.readLock().acquire();
} catch (InterruptedException ex) {
return false;
}
try {
for (int i = 0; (i < _containerCount) && !found; i++) {
if (_cache[i].containsValue(value)) { found = true; }
}
} catch (RuntimeException ex) {
throw ex;
} finally {
_lock.readLock().release();
}
return found;
}
/**
* {@inheritDoc}
*/
public final void clear() {
try {
_lock.writeLock().acquire();
} catch (InterruptedException ex) {
return;
}
try {
while (_containerCount > 0) { expireCacheContainer(); }
_cacheSize = 0;
} catch (RuntimeException ex) {
throw ex;
} finally {
_lock.writeLock().release();
}
}
//--------------------------------------------------------------------------
// view operations of map interface
/**
* {@inheritDoc}
*/
public final Set<Object> keySet() {
Set<Object> set = new HashSet<Object>(size());
try {
_lock.readLock().acquire();
} catch (InterruptedException ex) {
return set;
}
try {
for (int i = 0; i < _containerCount; i++) {
set.addAll(_cache[i].keySet());
}
} catch (RuntimeException ex) {
throw ex;
} finally {
_lock.readLock().release();
}
return set;
}
/**
* {@inheritDoc}
*/
public final Collection<Object> values() {
Collection<Object> col = new ArrayList<Object>(size());
try {
_lock.readLock().acquire();
} catch (InterruptedException ex) {
return col;
}
try {
for (int i = 0; i < _containerCount; i++) {
col.addAll(_cache[i].values());
}
} catch (RuntimeException ex) {
throw ex;
} finally {
_lock.readLock().release();
}
return col;
}
/**
* {@inheritDoc}
*/
public final Set<Entry<Object, Object>> entrySet() {
Map<Object, Object> map = new Hashtable<Object, Object>(size());
try {
_lock.readLock().acquire();
} catch (InterruptedException ex) {
return map.entrySet();
}
try {
for (int i = 0; i < _containerCount; i++) {
map.putAll(_cache[i]);
}
} catch (RuntimeException ex) {
throw ex;
} finally {
_lock.readLock().release();
}
return map.entrySet();
}
//--------------------------------------------------------------------------
// protected methods for concrete implementations
/**
* Get reference to the ReadWriteLock of this cache instance.
*
* @return ReadWriteLock to synchronize access to cache.
*/
protected final ReadWriteLock lock() { return _lock; }
/**
* Get object currently associated with given key from cache. Take care to acquire a
* read or write lock before calling this method and release the lock thereafter.
*
* @param key The key to return the associated object for.
* @return The object associated with given key.
*/
protected final Object getObjectFromCache(final Object key) {
Object result;
for (int i = 0; i < _containerCount; i++) {
result = _cache[i].get(key);
if (result != null) { return result; }
}
return null;
}
/**
* Put given value with given key in cache. Return the object previously associated
* with key. Take care to acquire a write lock before calling this method and release
* the lock thereafter.
*
* @param key The key to associate the given value with.
* @param value The value to associate with given key.
* @return The object previously associated with given key. <code>null</code> will
* be returned if no value has been associated with key.
*/
protected final Object putObjectIntoCache(final Object key, final Object value) {
// We first check if a new container have to be created. This is the case
// if there is none or if we have a capacity limit and the head container
// holds the maximum allowed number of entries.
if ((_containerCount == 0) || ((_cacheCapacity > 0)
&& (_cache[0].size() >= _containerCapacity))) {
addCacheContainer();
}
// Then we can put the new or updated one to the head of the belt.
Object result = _cache[0].put(key, value);
if (result != null) { return result; }
// If result is null we have to search the other containers of the
// cache if they contain an entry for the key, in which case we have
// to remove that old entry.
for (int i = 1; (i < _containerCount) && (result == null); i++) {
result = _cache[i].remove(key);
}
if (result != null) { return null; }
// If result still is null we have added a new entry and need to
// increment size of the whole cache. As size has increased we also
// need to check if size exceeds capacity limit, in which case we
// have to expire the oldest container of the cache.
_cacheSize++;
if (_cacheCapacity > 0) {
while (_cacheCapacity < _cacheSize) {
expireCacheContainer();
}
}
return result;
}
/**
* Remove any available association for given key. Take care to acquire a write lock
* before calling this method and release the lock thereafter.
*
* @param key The key to remove any previously associate value for.
* @return The object previously associated with given key. <code>null</code> will
* be returned if no value has been associated with key.
*/
protected final Object removeObjectFromCache(final Object key) {
Object result;
for (int i = 0; i < _containerCount; i++) {
result = _cache[i].remove(key);
if (result != null) {
_cacheSize--;
return result;
}
}
return null;
}
//--------------------------------------------------------------------------
// private helper methods
/**
* Recalculate the number of entries in the cache by summing the size of all
* containers.
*/
private void recalcCacheSize() {
int size = 0;
for (int i = 0; i < _containerCount; i++) {
size += _cache[i].size();
}
_cacheSize = size;
}
/**
* Add an empty container from the pool to the cache.
*/
private void addCacheContainer() {
Container[] temp = new Container[++_containerCount];
System.arraycopy(_cache, 0, temp, 1, _cache.length);
temp[0] = _pool[--_poolCount];
temp[0].updateTimestamp();
_cache = temp;
}
/**
* Remove the oldest container from the cache and pass it to the configured reaper
* to do its expiration work. Then clear the container and put it back into the
* pool for further use.
*/
private void expireCacheContainer() {
Container expired = _cache[--_containerCount];
Container[] temp = new Container[_containerCount];
System.arraycopy(_cache, 0, temp, 0, _containerCount);
_cache = temp;
_cacheSize -= expired.size();
_reaper.handleExpiredContainer(expired);
expired.clear();
_pool[_poolCount++] = expired;
}
/**
* Check the containers of the cache if their ttl has been expired. If the ttl of
* the oldest container has expired the expireCacheContainer() method is called to
* remove it. After removing it, the next container will be checked until one is
* found thats ttl has not expired.
*/
private void timeoutCacheContainers() {
long timeout = System.currentTimeMillis() - _ttl;
while ((_containerCount > 0)
&& (_cache[_containerCount - 1].getTimestamp() <= timeout)) {
expireCacheContainer();
}
}
//--------------------------------------------------------------------------
// private helper classes
/**
* TimerTask that checks if ttl of containers in the cache has expired. If some
* of them have expired they are removed. One new container will be added to
* the head of the cache. In addition the size of the cache is recalculated. This
* is not reqired for containers that hold strong references to their objects
* but for those containers holding soft or weak references its the only way to
* adjust the size with regard to the objects that have been garbage collected.
* <p>
* The interval this task will be executed is set to the ttl divided by the
* number of containers in the cache. If ttl has been configured to be 0 the
* ExpirationTask will never be executed.
*/
private static class ExpirationTask extends TimerTask {
/** Reference to the hashbelt this ExpirationTask belongs to. */
private AbstractHashbelt _owner;
/**
* Construct a new ExpirationTask for the given hashbelt.
*
* @param owner The hashbelt this ExpirationTask belongs to.
*/
public ExpirationTask(final AbstractHashbelt owner) { _owner = owner; }
/**
* @see java.lang.Runnable#run()
*/
public void run() {
try {
_owner._lock.writeLock().acquire();
_owner.timeoutCacheContainers();
_owner.addCacheContainer();
_owner.recalcCacheSize();
} catch (ThreadDeath t) {
LOG.debug("Stopping expiration thread: " + _owner.getName());
throw t;
} catch (Throwable t) {
LOG.error("Caught exception during expiration: " + _owner.getName(), t);
if (t instanceof VirtualMachineError) { throw (VirtualMachineError) t; }
} finally {
_owner._lock.writeLock().release();
}
}
}
/**
* TimerTask that logs the number of containers and objects in the cache at the
* interval configured by the monitor parameter. If monitor parameter has been
* set to 0 the MonitoringTask will never be executed.
*/
private static class MonitoringTask extends TimerTask {
/** Reference to the hashbelt this MonitoringTask belongs to. */
private AbstractHashbelt _owner;
/**
* Construct a new MonitoringTask for the given hashbelt.
*
* @param owner The hashbelt this MonitoringTask belongs to.
*/
public MonitoringTask(final AbstractHashbelt owner) { _owner = owner; }
/**
* @see java.lang.Runnable#run()
*/
public void run() {
try {
_owner._lock.readLock().acquire();
LOG.info("Cache '" + _owner.getName() + "' "
+ "currently holds " + _owner._containerCount + " containers "
+ "with " + _owner._cacheSize + " objects.");
} catch (ThreadDeath t) {
LOG.debug("Stopping monitoring thread: " + _owner.getName());
throw t;
} catch (Throwable t) {
LOG.error("Caught exception during monitoring: " + _owner.getName(), t);
if (t instanceof VirtualMachineError) { throw (VirtualMachineError) t; }
} finally {
_owner._lock.readLock().release();
}
}
}
//--------------------------------------------------------------------------
}