/**
* Helios, OpenSource Monitoring
* Brought to you by the Helios Development Group
*
* Copyright 2007, Helios Development Group and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*
*/
package org.helios.apmrouter.util;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
/**
* <p>Title: TimeoutQueueMap</p>
* <p>Description: A map to store value that will be ejected after some period of time if not removed, notifying registered timeout listeners</p>
* <p>Company: Helios Development Group LLC</p>
* @author Whitehead (nwhitehead AT heliosdev DOT org)
* <p><code>org.helios.apmrouter.util.TimeoutQueueMap</code></p>
* @param <K> The key type for this map
* @param <V> The value type for this maps
*/
public class TimeoutQueueMap<K, V> implements Runnable, Map<K, V> {
/** The timeout queue */
protected final DelayQueue<TimeoutQueueMapKey<K, V>> timeOutQueue = new DelayQueue<TimeoutQueueMapKey<K, V>>();
/** The reference map */
protected final ConcurrentHashMap<K, V> referenceMap;
/** The default delay time */
protected final long defaultDelayTime;
/** The timeout thread */
protected final Thread timeoutThread;
/** A set of registered timeout listeners */
protected final Set<TimeoutListener<K, V>> timeOutListeners = new CopyOnWriteArraySet<TimeoutListener<K, V>>();
/** The number of timeout events that have occured */
protected final AtomicLong timeOutCount = new AtomicLong(0L);
/** A flag indicating that the timeout thread should keep running */
protected boolean running = true;
/** A serial number factory for timeout threads */
private static final AtomicLong serial = new AtomicLong(0L);
/** The thread group that timeout threads run in */
private static final ThreadGroup timeOutThreadGroup = new ThreadGroup(TimeoutQueueMap.class.getSimpleName() + "ThreadGroup");
/**
* Creates a new TimeoutQueueMap
* @param defaultDelayTime The default delay time in ms. for delayed puts
* @param initialCapacity the initial capacity. The implementation performs internal sizing to accommodate this many elements.
* @param loadFactor the load factor threshold, used to control resizing. Resizing may be performed when the average number of elements per bin exceeds this threshold.
* @param concurrencyLevel the estimated number of concurrently updating threads. The implementation performs internal sizing to try to accommodate this many threads.
*/
public TimeoutQueueMap(long defaultDelayTime, int initialCapacity, float loadFactor, int concurrencyLevel) {
referenceMap = new ConcurrentHashMap<K, V>(initialCapacity, loadFactor, concurrencyLevel);
this.defaultDelayTime = defaultDelayTime;
timeoutThread = new Thread(timeOutThreadGroup, this, TimeoutQueueMap.class.getSimpleName() + "Thread#" + serial.incrementAndGet());
timeoutThread.setDaemon(true);
timeoutThread.start();
}
/**
* Creates a new TimeoutQueueMap with the default concurrencyLevel (16).
* @param defaultDelayTime The default delay time in ms. for delayed puts
* @param initialCapacity the initial capacity. The implementation performs internal sizing to accommodate this many elements.
* @param loadFactor the load factor threshold, used to control resizing. Resizing may be performed when the average number of elements per bin exceeds this threshold.
*/
public TimeoutQueueMap(long defaultDelayTime, int initialCapacity, float loadFactor) {
this(defaultDelayTime, initialCapacity, loadFactor, 16);
}
/**
* Creates a new TimeoutQueueMap with default load factor (0.75) and concurrencyLevel (16).
* @param defaultDelayTime The default delay time in ms. for delayed puts
* @param initialCapacity the initial capacity. The implementation performs internal sizing to accommodate this many elements.
*/
public TimeoutQueueMap(long defaultDelayTime, int initialCapacity) {
this(defaultDelayTime, initialCapacity, 0.75f, 16);
}
/**
* Creates a new TimeoutQueueMap with a default initial capacity (16), load factor (0.75) and concurrencyLevel (16).
* @param defaultDelayTime The default delay time in ms. for delayed puts
*/
public TimeoutQueueMap(long defaultDelayTime) {
this(defaultDelayTime, 16, 0.75f, 16);
}
public static void main(String[] args) {
log("TimeoutQueueMap Test");
TimeoutQueueMap<Long, String> tqm = new TimeoutQueueMap<Long, String>(500);
tqm.addListener(new TimeoutListener<Long, String>(){
public void onTimeout(Long key, String value) {
log("TIMEOUT:[" + key + "][" + value + "]");
}
});
tqm.put(1L, "One");
String one = tqm.remove(1L);
log("Pulled One: [" + one + "] TQM Size:" + tqm.getSize() );
tqm.put(2L, "Two");
try { Thread.sleep(600); } catch (Exception e) {}
String two = tqm.remove(2L);
log("Two should be null:" + (two==null));
log(tqm);
}
public static void log(Object msg) {
System.out.println(msg);
}
/**
* Registers a timeout listener
* @param listener The listener to register
*/
public void addListener(TimeoutListener<K, V> listener) {
if(listener!=null) {
timeOutListeners.add(listener);
}
}
/**
* Unregisters a timeout listener
* @param listener The listener to unregister
*/
public void removeListener(TimeoutListener<K, V> listener) {
if(listener!=null) {
timeOutListeners.remove(listener);
}
}
/**
* Registers a name/value binding which will timeout in the passed delay time unless removed before the delay time
* @param key The key to retrieve the value by
* @param value The value to delay
* @param delayTime The timeout delay time in ms.
* @return The replaced value
*/
public synchronized V put(K key, V value, long delayTime) {
if(!running) throw new IllegalStateException("This TimeoutQueueMap has been shutdown", new Throwable());
V oldValue = referenceMap.put(key, value);
if(oldValue!=null) {
timeOutQueue.remove(getKeyFor(key));
}
timeOutQueue.add(getKeyFor(key, value, delayTime));
return oldValue;
}
/**
* Registers a name/value binding if the passed key has not already been registered which will timeout in the passed delay time unless removed before the delay time
* @param key The key to retrieve the value by
* @param value The value to delay
* @param delayTime The timeout delay time in ms.
* @return Null if the registration occured, otherwise the value already associated with the key
*/
public synchronized V putIfAbsent(K key, V value, long delayTime) {
if(!running) throw new IllegalStateException("This TimeoutQueueMap has been shutdown", new Throwable());
V oldValue = referenceMap.putIfAbsent(key, value);
if(oldValue==null) {
timeOutQueue.add(getKeyFor(key, value, delayTime));
return null;
}
return oldValue;
}
/**
* @param key
* @param value
* @param delayTime
* @return
*/
protected TimeoutQueueMapKey<K, V> getKeyFor(K key, V value, long delayTime) {
return new TimeoutQueueMapKey<K, V>(key, value, delayTime);
}
/**
* Registers a name/value binding which will timeout in the default delay time unless removed before the delay time
* @param key The key to retrieve the value by
* @param value THe value to delay
*/
public V put(K key, V value) {
if(!running) throw new IllegalStateException("This TimeoutQueueMap has been shutdown", new Throwable());
return put(key, value, defaultDelayTime);
}
/**
* Registers a name/value binding if the passed key has not already been registered which will timeout in the default delay time unless removed before the delay time
* @param key The key to retrieve the value by
* @param value The value to delay
* @return Null if the registration occured, otherwise the value already associated with the key
*/
public V putIfAbsent(K key, V value) {
return putIfAbsent(key, value, defaultDelayTime);
}
/**
* Removes a delayed value from the timeout queue map.
* If the value is removed before it times out, the value is returned successfully.
* If the delay has expired, will return null
* @param key The delayed value key
* @return The delayed value if it has not timed out, null otherwise
*/
public V remove(Object key) {
TimeoutQueueMapKey<K, V> tKey = getKeyFor(key);
boolean removed = timeOutQueue.remove(tKey);
V value = referenceMap.remove(key);
if(!removed && value!=null) {
// This should never happen
//throw new IllegalStateException("The referenced value [" + value + "] with key [" + key + "] could not be removed from the timeout queue, but was retrieved from the map. Programmer Error ?", new Throwable());
return null;
}
return value;
}
/**
* Polls the timeout queue for timedout values
* {@inheritDoc}
* @see java.lang.Runnable#run()
*/
public void run() {
while(running) {
try {
TimeoutQueueMapKey<K, V> mapKey = timeOutQueue.take();
referenceMap.remove(mapKey.key);
timeOutCount.incrementAndGet();
for(TimeoutListener<K, V> listener: timeOutListeners) {
if(listener instanceof ValueFilteredTimeoutListener) {
ValueFilteredTimeoutListener<K,V> filteringListener = (ValueFilteredTimeoutListener<K,V>)listener;
if(filteringListener.include(mapKey.delayed)) {
filteringListener.onTimeout(mapKey.key, mapKey.delayed);
}
} else {
listener.onTimeout(mapKey.key, mapKey.delayed);
}
}
} catch (Exception e) {
if(!running) return;
}
}
}
/**
* Returns the number of timeouts that have occured
* @return the number of timeouts that have occured
*/
public long getTimeOutCount() {
return timeOutCount.get();
}
/**
* Returns the number of pending delayed items
* @return the number of pending delayed items
*/
public int getSize() {
return referenceMap.size();
}
public void shutdown() {
timeoutThread.interrupt();
referenceMap.clear();
timeOutQueue.clear();
}
/**
* Purges thus timeout queue map and removes all entries.
* A bit iffy thread-wise and has race condition issues.
*/
public synchronized void purge(){
timeOutQueue.clear();
referenceMap.clear();
}
TimeoutQueueMapKey getKeyFor(Object key) {
return new TimeoutQueueMapKey(key, null, -1);
}
/**
* <p>Title: TimeoutQueueMapKey</p>
* <p>Description: A timestamped {@link Delayed} wrapper around a referenced object</p>
* <p>Company: Helios Development Group LLC</p>
* @author Whitehead (nwhitehead AT heliosdev DOT org)
* <p><code>org.helios.patterns.queues.TimeoutQueueMap.TimeoutQueueMapKey</code></p>
*/
protected class TimeoutQueueMapKey<K, V> implements Delayed {
/** The referenced delay object */
protected final V delayed;
/** The referenced delay object key */
protected final K key;
/** The timestamp of the creation of this delayed instance */
protected final long timestamp;
/**
* Creates a new TimeoutQueueMapKey
* @param key The delayed value key
* @param delayed The object to wrap as a delay
* @param delayTime The number of milliseconds to delay this object for
*/
public TimeoutQueueMapKey(K key, V delayed, long delayTime) {
this.delayed = delayed;
this.key = key;
this.timestamp = System.currentTimeMillis() + delayTime;
}
/**
* Compares this delayed with the specified delayed for order.
* Returns a negative integer, zero, or a positive integer as this object is less than, equal to,
* or greater than the specified object.
* @see java.lang.Comparable#compareTo(java.lang.Object)
*/
@Override
public int compareTo(Delayed delayed) {
long thisDelay = getDelay(TimeUnit.MILLISECONDS);
long thatDelay = delayed.getDelay(TimeUnit.MILLISECONDS);
return thisDelay <= thatDelay ? -1: 1;
}
/**
* Returns the time remaining on the delay for this object.
* {@inheritDoc}
* @see java.util.concurrent.Delayed#getDelay(java.util.concurrent.TimeUnit)
*/
@Override
public long getDelay(TimeUnit unit) {
return unit.convert((timestamp-System.currentTimeMillis()), TimeUnit.MILLISECONDS);
}
/**
* Constructs a <code>String</code> with all attributes
* in name = value format.
*
* @return a <code>String</code> representation
* of this object.
*/
public String toString() {
final String TAB = "\n\t";
StringBuilder retValue = new StringBuilder("TimeoutQueueMapKey [")
.append(TAB).append("key:").append(this.key)
.append(TAB).append("delayed:").append(this.delayed)
.append(TAB).append("timestamp:").append(this.timestamp)
.append(TAB).append("delay:").append(getDelay(TimeUnit.MILLISECONDS)).append(" ms.")
.append("\n]");
return retValue.toString();
}
/**
* Returns
* @return the delayed
*/
public V getDelayed() {
return delayed;
}
/**
* Returns
* @return the key
*/
public K getKey() {
return key;
}
/**
* Returns
* @return the timestamp
*/
public long getTimestamp() {
return timestamp;
}
/**
* {@inheritDoc}
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((key == null) ? 0 : key.hashCode());
return result;
}
/**
* {@inheritDoc}
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (obj == null)
return false;
if(obj instanceof TimeoutQueueMapKey) {
TimeoutQueueMapKey other = (TimeoutQueueMapKey)obj;
return key.equals(other.key);
}
return false;
}
private TimeoutQueueMap getOuterType() {
return TimeoutQueueMap.this;
}
}
/**
* Constructs a <code>String</code> with all attributes
* in name = value format.
*
* @return a <code>String</code> representation
* of this object.
*/
public String toString() {
final String TAB = "\n\t";
StringBuilder retValue = new StringBuilder("TimeoutQueueMap [")
.append(TAB).append("timeOutQueue:").append(this.timeOutQueue)
.append(TAB).append("referenceMap:").append(this.referenceMap)
.append(TAB).append("defaultDelayTime:").append(this.defaultDelayTime)
.append(TAB).append("timeoutThread:").append(this.timeoutThread)
.append(TAB).append("timeOutListeners:").append(this.timeOutListeners)
.append(TAB).append("timeOutCount:").append(this.timeOutCount)
.append("\n]");
return retValue.toString();
}
/**
* @return
* @see java.util.Map#size()
*/
public int size() {
return referenceMap.size();
}
/**
* @return
* @see java.util.Map#isEmpty()
*/
public boolean isEmpty() {
return referenceMap.isEmpty();
}
/**
* @param key
* @return
* @see java.util.Map#containsKey(java.lang.Object)
*/
public boolean containsKey(Object key) {
return referenceMap.containsKey(key);
}
/**
* @param value
* @return
* @see java.util.Map#containsValue(java.lang.Object)
*/
public boolean containsValue(Object value) {
return referenceMap.containsValue(value);
}
/**
* @param key
* @return
* @see java.util.Map#get(java.lang.Object)
*/
public V get(Object key) {
return referenceMap.get(key);
}
/**
* Puts all the elements in the passed map into this map
* @param m The map of members to add
* @see java.util.Map#putAll(java.util.Map)
*/
public void putAll(Map<? extends K, ? extends V> m) {
if(!running) throw new IllegalStateException("This TimeoutQueueMap has been shutdown", new Throwable());
for(Map.Entry<? extends K, ? extends V> entry: m.entrySet()) {
put(entry.getKey(), entry.getValue());
}
referenceMap.putAll(m);
}
/**
* Puts all the elements in the passed map into this map
* @param m The map of members to add
* @param timeout The timeout in ms. of the elements being added
* @see java.util.Map#putAll(java.util.Map)
*/
public void putAll(Map<? extends K, ? extends V> m, long timeout) {
if(!running) throw new IllegalStateException("This TimeoutQueueMap has been shutdown", new Throwable());
for(Map.Entry<? extends K, ? extends V> entry: m.entrySet()) {
put(entry.getKey(), entry.getValue(), timeout);
}
referenceMap.putAll(m);
}
/**
*
* @see java.util.Map#clear()
*/
public void clear() {
purge();
}
/**
* @return
* @see java.util.Map#keySet()
*/
public Set<K> keySet() {
return referenceMap.keySet();
}
/**
* @return
* @see java.util.Map#values()
*/
public Collection<V> values() {
return referenceMap.values();
}
private class ReadOnlyEntry implements Entry<K,V> {
private final Entry<K,V> inner;
/**
* Creates a new ReadOnlyEntry
* @param inner
*/
public ReadOnlyEntry(java.util.Map.Entry<K, V> inner) {
this.inner = inner;
}
public K getKey() {
return inner.getKey();
}
public V getValue() {
return inner.getValue();
}
public V setValue(V value) {
throw new UnsupportedOperationException("Setting values in Entries is not supported", new Throwable());
}
}
/**
* Returns a read only entry set
* @return a read only entry set
* @see java.util.Map#entrySet()
*/
public Set<Entry<K, V>> entrySet() {
Set<Entry<K, V>> entrySet = new HashSet<Entry<K, V>>(size());
for(Entry<K,V> entry: referenceMap.entrySet()) {
entrySet.add(new ReadOnlyEntry(entry));
}
return entrySet;
}
/**
* @param o
* @return
* @see java.util.Map#equals(java.lang.Object)
*/
public boolean equals(Object o) {
return referenceMap.equals(o);
}
/**
* @return
* @see java.util.Map#hashCode()
*/
public int hashCode() {
return referenceMap.hashCode();
}
}