/* * RHQ Management Platform * Copyright (C) 2005-2008 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation version 2 of the License. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ package org.rhq.enterprise.communications.command.client; import java.util.Collection; import java.util.Iterator; import java.util.LinkedList; import java.util.NoSuchElementException; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.Semaphore; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import mazz.i18n.Logger; import org.rhq.enterprise.communications.i18n.CommI18NFactory; import org.rhq.enterprise.communications.i18n.CommI18NResourceKeys; /** * This is the queue that will be used by {@link ClientCommandSender} to queue up command tasks that have to be sent. * The queue implements {@link BlockingQueue}; if you instantiate it with a capacity of 0 or less, it will actually be * unbounded (i.e. theoretical capacity at {@link Integer#MAX_VALUE}). * * <p>This queue supports "throttling", that is, only permits X number of commands at most to be taken from the queue in * Y number of milliseconds. Throttling is disabled unless specifically enabled in the configuration given this object * when this object is constructed or by calling {@link #enableQueueThrottling()}. Throttling affects * {@link #poll(long)} and {@link #take()}. Throttling does not affect how fast you can add commands to the queue - you * can queue commands as fast as you can.</p> * * <p>Because this is going to be a work queue for a {@link ThreadPoolExecutor}, and because of the strict type required * of work queues in its constructor, I must implement BlockingQueue<Runnable>, but I really wanted * <ClientCommandSenderTask> because the runnables stored in this queue should really only be of type * {@link ClientCommandSenderTask}.</p> * * @author John Mazzitelli */ class CommandQueue implements BlockingQueue<Runnable> { /** * Logger */ private static final Logger LOG = CommI18NFactory.getLogger(CommandQueue.class); /** * The real queue that contains commands that need to be sent. We delegate to this object. */ private LinkedBlockingQueue<Runnable> m_queue; /** * The semaphore that helps throttle taking things off the queue. Will be <code>null</code> then throttling is * disabled. */ private Semaphore m_throttleSemaphore; /** * Used for its monitor lock for throttling - no thread can change the throttle mode or aquire/release a semaphore * permit without first owning this lock. */ private Object m_throttleLock; /** * If queue throttling is enabled, this is the maximum number of commands that can be sent during the burst period. */ private long m_queueThrottleMaxCommands; /** * If queue throttling is enabled, this is the length of time (in milliseconds) of the burst period. */ private long m_queueThrottleBurstPeriodDurationMillis; /** * This will be the number of threads waiting to acquire a semaphore permit. Only used when we need to disable * throttling - we have to know how many threads we need to wake up out of the semaphore acquire method. */ private AtomicInteger m_waitingForAcquire; /** * Constructor for {@link CommandQueue} that provides a bounded queue with a maximum capacity specified in the * configuration. * * <p>If the queue capacity (see {@link ClientCommandSenderConfiguration#queueSize}) is less than or equal to 0, the * queue will be <b>unbounded</b>. Be careful using an unbounded queue since it may grow to use up alot of (or all) * available memory.</p> * * @param config the configuration that tells this queue things like its capacity and queue throttling settings. * * @throws IllegalArgumentException if config is <code>null</code> */ public CommandQueue(ClientCommandSenderConfiguration config) { if (config == null) { throw new IllegalArgumentException(LOG.getMsgString(CommI18NResourceKeys.COMMAND_QUEUE_NULL_CONFIG)); } m_throttleLock = new Object(); m_throttleSemaphore = null; m_waitingForAcquire = new AtomicInteger(0); if (config.queueSize > 0) { m_queue = new LinkedBlockingQueue<Runnable>(config.queueSize); } else { m_queue = new LinkedBlockingQueue<Runnable>(); } setQueueThrottleParameters(config); if (config.enableQueueThrottling) { enableQueueThrottling(); } else { disableQueueThrottling(); } return; } /** * Allows the caller to modify this queue's throttling configuration - specifically * {@link ClientCommandSenderConfiguration#queueThrottleMaxCommands} and * {@link ClientCommandSenderConfiguration#queueThrottleBurstPeriodMillis}. If queue throttling is already enabled, * these will take effect as soon as possible (though not necessarily immediately). * * @param config the object that contains this object's new configuration */ public void setQueueThrottleParameters(ClientCommandSenderConfiguration config) { synchronized (m_throttleLock) { m_queueThrottleMaxCommands = config.queueThrottleMaxCommands; m_queueThrottleBurstPeriodDurationMillis = config.queueThrottleBurstPeriodMillis; LOG.debug(CommI18NResourceKeys.COMMAND_QUEUE_CONFIGURED, m_queueThrottleMaxCommands, m_queueThrottleBurstPeriodDurationMillis); } return; } /** * This enables throttling which limits the number of commands that can be taken from the queue. A maximum number of * commands can be taken from the queue in the burst period. After that, the {@link #take()} and {@link #poll(long)} * will block until the throttling enables more commands to be taken from the queue. */ public void enableQueueThrottling() { synchronized (m_throttleLock) { // if we are already enabled for throttling, let's disable it so we can reconfigure our semaphore and thread if (isThrottlingEnabled()) { disableQueueThrottling(); } m_throttleSemaphore = new Semaphore((int) m_queueThrottleMaxCommands); ThrottleRunnable throttleRunnable = new ThrottleRunnable((int) m_queueThrottleMaxCommands, m_queueThrottleBurstPeriodDurationMillis); Thread thread = new Thread(throttleRunnable, "RHQ Command Queue Throttle Thread"); thread.setDaemon(true); thread.start(); LOG.debug(CommI18NResourceKeys.COMMAND_QUEUE_ENABLED, m_queueThrottleMaxCommands, m_queueThrottleBurstPeriodDurationMillis); } return; } /** * Disables throttling - taking from the queue will now occur as fast as possible. */ public void disableQueueThrottling() { synchronized (m_throttleLock) { if (m_throttleSemaphore != null) { // let any and all threads currently waiting to acquire a throttle semaphore to get one m_throttleSemaphore.release(m_waitingForAcquire.get() + 1000); // add a 1000 just for good measure } m_throttleSemaphore = null; m_throttleLock.notifyAll(); // this should kill the throttle thread immediately } LOG.debug(CommI18NResourceKeys.COMMAND_QUEUE_DISABLED); return; } /** * Returns <code>true</code> if throttling is enabled; <code>false</code> if commands can be taken from the queue as * fast as possible. * * @return <code>true</code> if throttling is enabled; <code>false</code> otherwise */ public boolean isThrottlingEnabled() { synchronized (m_throttleLock) { return m_throttleSemaphore != null; } } public void put(Runnable obj) throws InterruptedException { m_queue.put(obj); } public boolean offer(Runnable obj, long timeout, TimeUnit timeunit) throws InterruptedException { return m_queue.offer(obj, timeout, timeunit); } public boolean offer(Runnable o) { try { return offer(o, 0L, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return false; } } /** * If throttling is enabled, the attempt to take the command from the queue will occur only after this thread is * allowed to do so under the throttling algorithm. * * @return the command that was taken * * @throws InterruptedException */ public Runnable take() throws InterruptedException { acquireSemaphorePermit(); Runnable ret_obj = m_queue.take(); return ret_obj; } /** * If throttling is enabled, the attempt to take the command from the queue will occur only after this thread is * allowed to do so under the throttling algorithm. Only then will the <code>timeout</code> counter start. * Therefore, when throttling is enabled, the <code>timeout</code> timer won't start until after the throttling * allows it to attempt to take from the queue. * * @param timeout * @param timeunit * * @return the command that was taken * * @throws InterruptedException */ public Runnable poll(long timeout, TimeUnit timeunit) throws InterruptedException { acquireSemaphorePermit(); Runnable ret_val = m_queue.poll(timeout, timeunit); return ret_val; } /** * If throttling is enabled, the attempt to take the command from the queue will occur only after this thread is * allowed to do so under the throttling algorithm. Only then will the <code>timeout</code> counter start. * Therefore, when throttling is enabled, the <code>timeout</code> timer won't start until after the throttling * allows it to attempt to take from the queue. * * @see java.util.Queue#poll() */ public Runnable poll() { try { return poll(0, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return null; } } public Runnable peek() { return m_queue.peek(); // bypasses throttle } /** * This quickly drains the queue. Throttling is ignored even if throttling is enabled, this will drain all items as * fast as possible. * * @see BlockingQueue#drainTo(Collection, int) */ public int drainTo(Collection<? super Runnable> c, int maxElements) { return m_queue.drainTo(c, maxElements); } /** * This quickly drains the queue. Throttling is ignored even if throttling is enabled, this will drain all items as * fast as possible. Upon return, the queue will be empty. * * <p>Call this method when you are sure no threads will be placing items on the queue, otherwise, this will never * return as it will continue to drain until there are no more items left in the queue.</p> * * @see BlockingQueue#drainTo(Collection) */ public int drainTo(Collection<? super Runnable> c) { return m_queue.drainTo(c); } public int remainingCapacity() { return m_queue.remainingCapacity(); } public boolean add(Runnable o) { if (!offer(o)) { throw new IllegalStateException(); } return true; } public boolean addAll(Collection<? extends Runnable> c) { boolean changed = false; for (Runnable task : c) { if (!offer(task)) { throw new IllegalArgumentException("Cannot add: " + task); } else { changed = true; } } return changed; } public void clear() { drainTo(new LinkedList<Runnable>()); } public boolean contains(Object o) { return m_queue.contains(o); } public boolean containsAll(Collection<?> c) { return m_queue.containsAll(c); } public Runnable element() { return m_queue.element(); } public boolean isEmpty() { return m_queue.isEmpty(); } public Iterator<Runnable> iterator() { return m_queue.iterator(); } public Runnable remove() { Runnable task = poll(); if (task == null) { throw new NoSuchElementException(); } return task; } public boolean remove(Object o) { return m_queue.remove(o); // bypasses throttle } public boolean removeAll(Collection<?> c) { return m_queue.removeAll(c); // bypasses throttle } public boolean retainAll(Collection<?> c) { return m_queue.retainAll(c); // bypasses throttle } public int size() { return m_queue.size(); } public Object[] toArray() { return m_queue.toArray(); } public <T> T[] toArray(T[] a) { return m_queue.toArray(a); } /** * Acquires a semaphore permit, but only if throttling is enabled. * * @throws InterruptedException if thread was interrupted while blocked trying to acquire the permit */ private void acquireSemaphorePermit() throws InterruptedException { Semaphore semaphore = null; // we do NOT want to block in the acquire() inside of the synchronized block // just get the semaphore object and exit the sync block - we'll acquire outside of it // this will allow the throttle lock to be acquired should someone want to disable throttling // while we are waiting to acquire the semaphore permit synchronized (m_throttleLock) { if (m_throttleSemaphore != null) { semaphore = m_throttleSemaphore; m_waitingForAcquire.incrementAndGet(); } } if (semaphore != null) { semaphore.acquire(); m_waitingForAcquire.decrementAndGet(); } return; } /** * This thread sleeps for X milliseconds and when it awakes, releases N semaphore permits. This performs the * throttling. */ private class ThrottleRunnable implements Runnable { private long m_sleepMillis; private int m_numPermits; /** * Constructor for {@link ThrottleRunnable}. * * @param num_permits the maximum number of semaphore permits to release each cycle * @param sleep_millis the number of milliseconds to sleep before releasing semaphore permits */ public ThrottleRunnable(int num_permits, long sleep_millis) { m_numPermits = num_permits; m_sleepMillis = sleep_millis; } /** * @see java.lang.Runnable#run() */ public void run() { synchronized (m_throttleLock) { while (m_throttleSemaphore != null) { try { m_throttleLock.wait(m_sleepMillis); } catch (InterruptedException e) { // told to wake up, throttling was probably disabled // exit the thread break; } if (m_throttleSemaphore != null) { // make sure only at most m_numPermits are available to be acquired m_throttleSemaphore.release(m_numPermits - m_throttleSemaphore.availablePermits()); } } } // the queue was told not to throttle anymore, exit the thread return; } } }