package edu.brown.hstore.util;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.log4j.Logger;
import edu.brown.logging.LoggerUtil;
import edu.brown.logging.LoggerUtil.LoggerBoolean;
import edu.brown.profilers.ProfileMeasurement;
import edu.brown.utils.StringUtil;
/**
* Creates a wrapper around a queue that provides a dynamic limit on the
* size of the queue. You can specify whether this limit is allowed to increase,
* how much it should increase by, and what the max limit is.
* You can attach this wrapper to an EventObservable that will tell us
* when it is time to increase the limit.
* @author pavlo
* @param <E>
*/
public class ThrottlingQueue<E> implements Queue<E> {
private static final Logger LOG = Logger.getLogger(ThrottlingQueue.class);
private static final LoggerBoolean debug = new LoggerBoolean();
private static final LoggerBoolean trace = new LoggerBoolean();
static {
LoggerUtil.attachObserver(LOG, debug, trace);
}
// ----------------------------------------------------------------------------
// INTERNAL DATA MEMBERS
// ----------------------------------------------------------------------------
/**
* The internal queue that we're going to wrap this ThrottlingQueue around.
*/
private final Queue<E> queue;
/**
* This is a thread-safe approximation of what the current size of the
* queue. It's not meant to be completely accurate, but just good enough
* to get an idea of whether the queue is overloaded.
*/
private final AtomicInteger size = new AtomicInteger(0);
/**
* If this flag is set to true, then this queue is currently throttled.
* It will not be allowed to take in new elements until the size goes
* below the throttleRelease,
*/
private boolean throttled;
private int origThrottleThreshold;
private int throttleThreshold;
private int throttleRelease;
private double throttleReleaseFactor;
private int autoDelta;
private int autoMinSize;
private int autoMaxSize;
/**
* If this flag is set to true, then the ThrottlingQueue will automatically
* increase the throttleThreshold by throttleThresholdIncrease any time the
* queue is completely empty.
*/
private boolean allowIncreaseOnZero = false;
/**
* If this flag is set to true, then the ThrottlingQueue will automatically
* decrease the throttleThreshold by throttleThresholdDelta any time the
* queue gets throttled.
*/
private boolean allowDecreaseOnThrottle = false;
private final ProfileMeasurement throttle_time = new ProfileMeasurement("THROTTLING");
private boolean throttle_time_enabled = false;
// ----------------------------------------------------------------------------
// CONSTRUCTORS
// ----------------------------------------------------------------------------
/**
* Constructor
* @param queue The original queue
* @param throttleThreshold The initial max size of the queue before it is throttled.
* @param throttleReleaseFactor The release factor for when the queue will be unthrottled.
* @param autoDelta The increase delta for when we will increase the max size.
* @param autoMaxSize The maximum size of the queue.
*/
public ThrottlingQueue(Queue<E> queue,
int throttleThreshold,
double throttleReleaseFactor,
int autoDelta,
int autoMinSize,
int autoMaxSize) {
this.queue = queue;
this.throttled = false;
this.throttleThreshold = throttleThreshold;
this.origThrottleThreshold = throttleThreshold;
this.throttleReleaseFactor = throttleReleaseFactor;
this.autoDelta = autoDelta;
this.autoMinSize = autoMinSize;
this.autoMaxSize = autoMaxSize;
this.computeReleaseThreshold();
this.checkThrottling(this.allowDecreaseOnThrottle, this.size());
}
/**
* Constructor with threshold auto-increase disbled.
* @param queue The original queue
* @param throttleThreshold The initial max size of the queue before it is throttled.
* @param throttleReleaseFactor The release factor for when the queue will be unthrottled.
*/
public ThrottlingQueue(Queue<E> queue,
int throttleThreshold,
double throttleReleaseFactor) {
this(queue, throttleThreshold, throttleReleaseFactor, 0, throttleThreshold, throttleThreshold);
this.allowIncreaseOnZero = false;
this.allowDecreaseOnThrottle = false;
}
// ----------------------------------------------------------------------------
// INTERNAL METHODS
// ----------------------------------------------------------------------------
private void computeReleaseThreshold() {
this.throttleRelease = Math.max((int)(this.throttleThreshold * this.throttleReleaseFactor), 1);
}
/**
* Check whether the size of this queue is greater than our max limit.
* We don't need to worry if this is 100% accurate, so we won't block here
* @param can_change
* @param last_size
*/
private void checkThrottling(boolean can_change, int last_size) {
// If they're not throttled, then we should check whether
// we need to throttle them
if (this.throttled == false) {
// If they've gone above the current queue max size, then
// they are throtttled!
if (last_size >= this.throttleThreshold) {
if (this.throttle_time_enabled) this.throttle_time.start();
if (can_change && this.allowDecreaseOnThrottle) {
if (trace.val) LOG.trace("throttleThreshold=>"+this.throttleThreshold);
synchronized (this.size) {
if (this.throttled == false) {
this.throttleThreshold = Math.max(this.autoMinSize, (this.throttleThreshold - this.autoDelta));
this.computeReleaseThreshold();
}
this.throttled = true;
} // SYNCH
} else {
this.throttled = true;
}
}
// Or if the queue is completely empty and we're allowe to increase
// the max limit, then we'll go ahead and do that for them here
else if (can_change && last_size == 0 && this.allowIncreaseOnZero) {
if (trace.val) LOG.trace("throttleThreshold=>"+this.throttleThreshold);
synchronized (this.size) {
this.throttleThreshold = Math.min(this.autoMaxSize, (this.throttleThreshold + this.autoDelta));
this.computeReleaseThreshold();
} // SYNCH
}
}
// If we're throttled and we've gone below our release
// threshold, then we can go ahead and unthrottle them
else if (last_size <= this.throttleRelease) {
if (debug.val)
LOG.debug(String.format("Unthrottling queue [size=%d > release=%d]",
last_size, this.throttleRelease));
if (this.throttle_time_enabled) this.throttle_time.stopIfStarted();
this.throttled = false;
}
}
// ----------------------------------------------------------------------------
// UTILITY METHODS
// ----------------------------------------------------------------------------
public void reset() {
this.throttleThreshold = this.origThrottleThreshold;
this.throttled = false;
int new_size = this.queue.size();
this.size.lazySet(new_size);
this.checkThrottling(false, new_size);
}
/**
* Returns true if this queue is currently throttled.
* @return
*/
public boolean isThrottled() {
return (this.throttled);
}
/**
* Get the maximum size of the queue before it will throttle new additions.
* @return
*/
public int getThrottleThreshold() {
return (this.throttleThreshold);
}
/**
* Set the maximum size of the queue before it will throttle new additions.
* @param threshold
*/
public void setThrottleThreshold(int threshold) {
assert(threshold > 0);
this.throttleThreshold = threshold;
this.origThrottleThreshold = threshold;
this.computeReleaseThreshold();
}
public void setThrottleReleaseFactor(double queue_release_factor) {
this.throttleReleaseFactor = queue_release_factor;
this.computeReleaseThreshold();
}
public double getThrottleReleaseFactor() {
return (this.throttleReleaseFactor);
}
/**
* Return the size of the queue that much be reached before a
* throttled queue will be allowed to take new additions.
* @return
*/
public int getThrottleRelease() {
return (this.throttleRelease);
}
/**
* Return the value that this ThrottlingQueue will automatically increase
* the throttle threshold if the size of this queue goes to zero.
* This will only matter if allow_increase is set to true.
* @return
*/
public int getThrottleThresholdIncreaseDelta() {
return (this.autoDelta);
}
/**
* Set the delta that the queue will automatically increase the throttle
* threshold if the size of this queue goes to zero.
* This will only matter if allow_increase is set to true.
* @param delta
*/
public void setThrottleThresholdAutoDelta(int delta) {
this.autoDelta = delta;
}
/**
* Set the min size of the throttling threshold. If this queue is allowed
* to decrease the threshold whenever the queue is throttled, then this is
* the minimum size that the threshold will be allowed to decreased to.
* @param size
*/
public void setThrottleThresholdMinSize(int size) {
this.autoMinSize = size;
}
public int getThrottleThresholdMinSize() {
return (this.autoMinSize);
}
/**
* Set the max size of the throttling threshold. If this queue is allowed
* to increase the threshold whenever the queue is empty, then this is
* the maximum size that the threshold will be allowed to increase to.
* @param size
*/
public void setThrottleThresholdMaxSize(int size) {
this.autoMaxSize = size;
}
public int getThrottleThresholdMaxSize() {
return (this.autoMaxSize);
}
/**
* If the allow_increase flag is set to true, then the
* throttle threshold will be allowed to automatically grow
* whenever the the size of the queue is zero.
* @param allow
*/
public void setAllowIncrease(boolean allow) {
this.allowIncreaseOnZero = allow;
}
/**
* If the allow_decrease flag is set to true, then the
* throttle threshold will be allowed to automatically decrease
* whenever the queue is throttled.
* @param allow
*/
public void setAllowDecrease(boolean allow) {
this.allowDecreaseOnThrottle = allow;
}
protected final Queue<E> getQueue() {
return (this.queue);
}
public void enableProfiling(boolean val) {
this.throttle_time_enabled = val;
}
public ProfileMeasurement getThrottleTime() {
return (this.throttle_time);
}
// ----------------------------------------------------------------------------
// THROTTLING QUEUE API METHODS
// ----------------------------------------------------------------------------
/**
* Offer an element to the queue. If force is true, then the element will
* always be inserted and not count against the throttling counter.
* @param e
* @param force
* @return
*/
public boolean offer(E e, boolean force) {
boolean ret = false;
if (force) {
this.queue.add(e);
ret = true;
} else if (this.throttled == false) {
ret = this.queue.offer(e);
}
if (ret) {
int size = this.size.incrementAndGet();
// If they had us force the element into the queue, then
// we won't check to see whether to enable throttling
if (force == false) {
// Since we're adding an item, we know that the size
// is unlikely to be zero.
this.checkThrottling(this.allowDecreaseOnThrottle, size);
}
}
return (ret);
}
// ----------------------------------------------------------------------------
// QUEUE API METHODS
// ----------------------------------------------------------------------------
@Override
public boolean add(E e) {
return (this.offer(e, false));
}
@Override
public boolean offer(E e) {
return this.offer(e, false);
}
@Override
public boolean remove(Object o) {
boolean ret = this.queue.remove(o);
if (ret) {
this.checkThrottling(this.allowIncreaseOnZero, this.size.decrementAndGet());
}
return (ret);
}
@Override
public E poll() {
E e = this.queue.poll();
if (e != null) {
this.checkThrottling(this.allowIncreaseOnZero, this.size.decrementAndGet());
}
return (e);
}
@Override
public E remove() {
E e = this.queue.remove();
if (e != null) {
this.checkThrottling(this.allowIncreaseOnZero, this.size.decrementAndGet());
}
return (e);
}
@Override
public void clear() {
if (this.throttle_time_enabled && this.throttled) this.throttle_time.stopIfStarted();
this.throttled = false;
this.size.set(0);
this.queue.clear();
this.checkThrottling(false, 0);
}
@Override
public boolean removeAll(Collection<?> c) {
boolean ret = this.queue.removeAll(c);
if (ret) {
int new_size = this.queue.size();
this.size.lazySet(new_size);
this.checkThrottling(this.allowIncreaseOnZero, new_size);
}
return (ret);
}
@Override
public boolean retainAll(Collection<?> c) {
boolean ret = this.queue.retainAll(c);
if (ret) {
int new_size = this.queue.size();
this.size.lazySet(new_size);
this.checkThrottling(this.allowIncreaseOnZero, new_size);
}
return (ret);
}
@Override
public boolean addAll(Collection<? extends E> c) {
boolean ret = this.queue.addAll(c);
if (ret) {
int new_size = this.queue.size();
this.size.lazySet(new_size);
this.checkThrottling(false, new_size);
}
return (ret);
}
@Override
public E element() {
return (this.queue.element());
}
@Override
public E peek() {
return (this.queue.peek());
}
@Override
public boolean contains(Object o) {
return (this.queue.contains(o));
}
@Override
public boolean containsAll(Collection<?> c) {
return (this.queue.containsAll(c));
}
@Override
public boolean isEmpty() {
return (this.queue.isEmpty());
}
/**
* Lock-free approximation of whether the underlying queue is empty
* This is not guaranteed to be correct.
* @return
*/
public boolean approximateIsEmpty() {
return (this.size.get() == 0);
}
@Override
public Iterator<E> iterator() {
return (this.queue.iterator());
}
@Override
public int size() {
int new_size = this.queue.size();
this.size.lazySet(new_size);
return (new_size);
}
@Override
public Object[] toArray() {
return (this.queue.toArray());
}
@Override
public <T> T[] toArray(T[] a) {
return (this.queue.toArray(a));
}
@Override
public String toString() {
return String.format("%s [max=%d / release=%d / delta=%d]",
this.queue.toString(),
this.throttleThreshold, this.throttleRelease, this.autoDelta);
}
public String debug() {
Map<String, Object> m = new LinkedHashMap<String, Object>();
m.put("Size", String.format("Actual=%d / Approx=%d", this.queue.size(), this.size.get()));
m.put("Throttled", this.throttled);
m.put("Threshold", this.throttleThreshold);
m.put("Release", String.format("%d [%.1f%%]", this.throttleRelease, this.throttleReleaseFactor*100));
m.put("Allow Decrease", this.allowIncreaseOnZero);
m.put("Allow Increase", this.allowIncreaseOnZero);
m.put("Increase Delta", this.autoDelta);
m.put("Increase Min", this.autoMinSize);
m.put("Increase Max", this.autoMaxSize);
return (StringUtil.formatMaps(m));
}
}