/** * 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.destination.accumulator; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.Set; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.ReentrantLock; import org.apache.log4j.Logger; import org.springframework.jmx.export.annotation.ManagedAttribute; /** * <p>Title: TimeSizeFlushQueue</p> * <p>Description: A queue that is flushed when triggered by a size threshold and/or an elapsed time between flushes.</p> * <p>Company: Helios Development Group LLC</p> * @author Whitehead (nwhitehead AT heliosdev DOT org) * <p><code>org.helios.apmrouter.destination.accumulator.TimeSizeFlushQueue</code></p> * @param <T> The expected type of the queued item */ public class TimeSizeFlushQueue<T> implements Runnable { /** The default scheduler for instances not provided one */ private static volatile ScheduledExecutorService defaultScheduler; /** The default threadPool for instances not provided one */ private static volatile ExecutorService defaultThreadPool; /** Creation lock for the default scheduler */ private static final Object schedulerLock = new Object(); /** Creation lock for the default threadPool */ private static final Object threadPoolLock = new Object(); /** The name of the flushQueue */ protected final String name; /** The queue size threshold */ protected final AtomicInteger sizeTrigger = new AtomicInteger(0); /** The elapsed time since last flush threshold in ms. */ protected final AtomicLong timeTrigger = new AtomicLong(0); /** The flush queue */ protected final BlockingQueue<T> queue; /** The flush queue runnable */ protected final FlushQueueReceiver<T> receiver; /** The flush running lock */ protected final ReentrantLock flushLock = new ReentrantLock(false); /** The scheduler for time triggered flushes */ protected final ScheduledExecutorService scheduler; /** The thread pool for processing flushes */ protected final ExecutorService flushThreadPool; /** The timer scheduled task handle */ protected ScheduledFuture<?> handle = null; /** The elapsed time in ms. of the last flush */ protected final AtomicLong lastFlushElapsed = new AtomicLong(0L); /** The total number of completed flushes */ protected final AtomicLong flushCount = new AtomicLong(0L); /** The total number of flush exceptions */ protected final AtomicLong flushExceptionCount = new AtomicLong(0L); /** The total number of queue drops on account of a full queue */ protected final AtomicLong queueDropCount = new AtomicLong(0L); /** Indicates that the size and time configuration does not support buffering and the enqueue will be bypassed */ protected final boolean bypassQueue; /** Instance logger */ protected final Logger log; /** The flush buffer which is the queue drains to synchronously in the trigger thread */ protected final Set<T> flushBuffer; /** static logger */ protected static final Logger LOG = Logger.getLogger(TimeSizeFlushQueue.class); /** * Creates the default scheduler * @return a scheduler */ private static ScheduledExecutorService getDefaultScheduler() { if(defaultScheduler==null) { synchronized(schedulerLock) { if(defaultScheduler==null) { defaultScheduler = Executors.newScheduledThreadPool(2, new ThreadFactory(){ private final AtomicInteger serial = new AtomicInteger(0); private final ThreadGroup threadGroup = new ThreadGroup("TimeSizeFlushQueueSchedulerThreadGroup"); @Override public Thread newThread(Runnable r) { Thread t = new Thread(threadGroup, r, "TimeSizeFlushQueueSchedulerThread#" + serial.incrementAndGet()); t.setDaemon(true); return t; } }); } } } return defaultScheduler; } /** * Creates the default thread pool * @return a thread pool */ private static ExecutorService getDefaultExecutor() { if(defaultThreadPool==null) { synchronized(threadPoolLock) { if(defaultThreadPool==null) { defaultThreadPool = Executors.newFixedThreadPool(5, new ThreadFactory(){ private final AtomicInteger serial = new AtomicInteger(0); private final ThreadGroup threadGroup = new ThreadGroup("TimeSizeFlushQueueThreadGroup"); @Override public Thread newThread(Runnable r) { Thread t = new Thread(threadGroup, r, "TimeSizeFlushQueueThread#" + serial.incrementAndGet()); t.setDaemon(true); return t; } }); } } } return defaultScheduler; } /** * Creates a new TimeSizeFlushQueue * @param name The name for this flushQueue * @param sizeTrigger The flush size trigger * @param timeTrigger The flush time trigger * @param receiver The receiver runnable responsible for processing the flush * @param scheduler An externally provided scheduler. If null, uses the default shared scheduler. * @param threadPool An externally provided thread pool. If null, uses the default shared pool. */ public TimeSizeFlushQueue(String name, int sizeTrigger, long timeTrigger, FlushQueueReceiver<T> receiver, ScheduledExecutorService scheduler, ExecutorService threadPool) { this.name = name; log = Logger.getLogger(getClass().getName() + "." + this.name); this.sizeTrigger.set(sizeTrigger); this.timeTrigger.set(timeTrigger); bypassQueue = (sizeTrigger<2 && timeTrigger<1); if(!bypassQueue) { queue = new ArrayBlockingQueue<T>(sizeTrigger+2, false); flushBuffer = new HashSet<T>(sizeTrigger+2); this.scheduler = scheduler==null ? getDefaultScheduler() : scheduler; } else { queue = null; flushBuffer = null; this.scheduler = null; } this.receiver = receiver; this.flushThreadPool = threadPool==null ? getDefaultExecutor() : threadPool; schedule(); log.info("Created TimeSizeFlushQueue [" + this.name + "]"); } /** * Creates a new TimeSizeFlushQueue using the default shared scheduler and thread pool. * @param name The name for this flushQueue * @param sizeTrigger The flush size trigger * @param timeTrigger The flush time trigger in ms. * @param receiver The receiver runnable responsible for processing the flush */ public TimeSizeFlushQueue(String name, int sizeTrigger, long timeTrigger, FlushQueueReceiver<T> receiver) { this(name, sizeTrigger, timeTrigger, receiver, null, null); } /** * Schedules the time flush callback. */ protected void schedule() { long time = timeTrigger.get(); if(time>0 && scheduler!=null) { handle = scheduler.schedule(new Runnable(){ @Override public void run() {timeFlush();}; }, time, TimeUnit.MILLISECONDS); if(log.isDebugEnabled()) log.debug("Scheduled for timed trigger in [" + time + "] ms."); } } /** * Determines if the size threshold has been met for a flush. * @return true if the size threshold has been met for a flush. */ protected boolean sizeTriggered() { int trig = sizeTrigger.get(); if(trig<2) return false; return queue.size()>=trig; } /** * Triggered when the size trigger is exceeded */ public void sizeFlush() { if(log.isDebugEnabled()) log.debug("Starting Size Triggered Flush"); boolean acquiredLock = false; try { acquiredLock = flushLock.tryLock(); if(!acquiredLock) { return; } if(handle!=null) handle.cancel(true); if(!queue.isEmpty()) { queue.drainTo(flushBuffer); flushThreadPool.execute(this); } } finally { try { schedule(); } catch (Exception e) { handle=null; LOG.fatal("Failed to reschedule timer trigger", e); } if(acquiredLock) { while(flushLock.getHoldCount()>0) { flushLock.unlock(); } } } } /** * Triggered when the flush time elapsed */ public void timeFlush() { if(log.isDebugEnabled()) log.debug("Starting Time Triggered Flush"); boolean acquiredLock = false; try { acquiredLock = flushLock.tryLock(); if(!acquiredLock) { return; } if(!queue.isEmpty()) { queue.drainTo(flushBuffer); flushThreadPool.execute(this); } } finally { try { schedule(); } catch (Exception e) { handle=null; LOG.fatal("Failed to reschedule timer trigger", e); } if(acquiredLock) { while(flushLock.getHoldCount()>0) { flushLock.unlock(); } } } } /** * Executes the flush */ @Override public void run() { int itemsToFlush = flushBuffer.size(); if(log.isDebugEnabled()) log.debug("Starting Flush of [" + itemsToFlush + "] items."); long start = System.currentTimeMillis(); try { if(itemsToFlush>0) { receiver.flushTo(flushBuffer); flushBuffer.clear(); } else { return; } } catch (Exception e) { flushExceptionCount.incrementAndGet(); } finally { lastFlushElapsed.set(System.currentTimeMillis()-start); flushCount.incrementAndGet(); } } /** * Calls the receiver with the passed items directly (i.e. not from the flushQueue) * @param items A collection of items to flush */ protected void directRun(final Collection<T> items) { flushThreadPool.execute(new Runnable(){ @Override public void run() { long start = System.currentTimeMillis(); try { if(items.size()>0) { receiver.flushTo(items); items.clear(); } else { return; } } catch (Exception e) { flushExceptionCount.incrementAndGet(); } finally { lastFlushElapsed.set(System.currentTimeMillis()-start); flushCount.incrementAndGet(); } } }); } /** * Adds an item to the queue. * @param t the item to add * @return true if the item was successfully processed * @see java.util.concurrent.BlockingQueue#add(java.lang.Object) */ public boolean add(T t) { if(t==null) return false; if(bypassQueue) { directRun(new ArrayList<T>(Arrays.asList(t))); return true; } boolean b = queue.add(t); if(sizeTriggered()) { sizeFlush(); } return b; } /** * Adds a collection of Ts to the queue * @param tcoll The collection of Ts to add. * @return true if the add succeeded, false if it did not. * @see java.util.Collection#addAll(java.util.Collection) */ public boolean addAll(Collection<T> tcoll) { if(tcoll==null) return true; if(bypassQueue) { directRun(tcoll); return true; } try { boolean b = queue.addAll(tcoll); if(!b) queueDropCount.addAndGet(tcoll.size()); if(sizeTriggered()) { flushThreadPool.execute(new Runnable(){ @Override public void run() {sizeFlush();}; }); } return b; } catch (Exception e) { queueDropCount.addAndGet(tcoll.size()); return false; } } /** * Offers a T to the queue, waiting the defined time to insert if the queue is full. * @param t The instance of T to offer * @param waitTime The time to wait to insert if the queue is full * @param unit The unit of time to wait * @return true if the insert succeeded. * @see java.util.concurrent.BlockingQueue#offer(java.lang.Object, long, java.util.concurrent.TimeUnit) */ public boolean offer(T t, long waitTime, TimeUnit unit) { if(t==null) return true; if(bypassQueue) { directRun(new ArrayList<T>(Arrays.asList(t))); return true; } try { boolean b = queue.offer(t, waitTime, unit); if(!b) queueDropCount.incrementAndGet(); if(sizeTriggered()) { flushThreadPool.execute(new Runnable(){ @Override public void run() {sizeFlush();}; }); } return b; } catch (Exception e) { queueDropCount.incrementAndGet(); return false; } } /** * Offers a T to the queue without waiting if the queue is full * @param t The instance of T to offer * @return true if the insert succeeded. * @see java.util.concurrent.BlockingQueue#offer(java.lang.Object) */ public boolean offer(T t) { if(t==null) return true; if(bypassQueue) { directRun(new ArrayList<T>(Arrays.asList(t))); return true; } try { boolean b = queue.offer(t); if(!b) queueDropCount.incrementAndGet(); if(sizeTriggered()) { flushThreadPool.execute(new Runnable(){ @Override public void run() {sizeFlush();}; }); } return b; } catch (Exception e) { queueDropCount.incrementAndGet(); return false; } } /** * Puts a T to the queue waiting if the queue is full * @param t The instance of T to offer * @throws InterruptedException Thrown if put is interrupted * @see java.util.concurrent.BlockingQueue#put(java.lang.Object) */ public void put(T t) throws InterruptedException { if(t==null) return; if(bypassQueue) { directRun(new ArrayList<T>(Arrays.asList(t))); return; } try { queue.put(t); if(sizeTriggered()) { flushThreadPool.execute(new Runnable(){ @Override public void run() {sizeFlush();}; }); } } catch (Exception e) { queueDropCount.incrementAndGet(); } } /** * Returns the size trigger threshold * @return the sizeTrigger */ @ManagedAttribute(description="The size queue flush trigger") public int getSizeTrigger() { return sizeTrigger.get(); } /** * Sets the size trigger * @param size the size to set the size triger */ public void setSizeTrigger(int size) { if(size<1) throw new IllegalArgumentException("Size cannot be less than one"); sizeTrigger.set(size); } /** * Returns the time trigger threshold in ms. * @return the timeTrigger */ @ManagedAttribute(description="The time queue flush trigger in ms.") public long getTimeTrigger() { return timeTrigger.get(); } /** * Sets the time trigger in ms. * @param time the time to set the time triger */ public void setTimeTrigger(long time) { if(time<1) throw new IllegalArgumentException("Time cannot be less than one"); timeTrigger.set(time); } /** * Returns the queue size * @return the queue size */ @ManagedAttribute(description="The number of items in the queue") public int getQueueSize() { return queue.size(); } /** * Returns the type of the flush receiver * @return the receiver class name */ @ManagedAttribute(description="The flush receiver type") public String getReceiverType() { return receiver.getClass().getName(); } /** * Returns the lock state of the flush lock * @return true if the flushLock is locked */ @ManagedAttribute(description="The state of the flush lock") public boolean getFlushLockState() { return flushLock.isLocked(); } /** * @return the scheduler */ public ScheduledExecutorService getScheduler() { return scheduler; } /** * @return the flushThreadPool */ public ExecutorService getFlushThreadPool() { return flushThreadPool; } /** * Returns the elapsed time of the last flush (ms.) * @return the lastFlushElapsed */ @ManagedAttribute(description="The elapsed time of the last flush (ms.)") public long getLastFlushElapsed() { return lastFlushElapsed.get(); } /** * Returns the total number of flush events * @return the flushCount */ @ManagedAttribute(description="The number of flush events") public long getFlushCount() { return flushCount.get(); } /** * Returns the number of flush exceptions * @return the flushExceptionCount */ @ManagedAttribute(description="The number of flush exceptions") public long getFlushExceptionCount() { return flushExceptionCount.get(); } /** * Returns the number of items dropped on account of a full flush queue * @return the queueDropCount */ @ManagedAttribute(description="The number of items dropped on account of a full flush queue") public long getQueueDropCount() { return queueDropCount.get(); } /** * Returns this flushQueue's name * @return the name */ @ManagedAttribute(description="The name of this flush queue") public String getName() { return name; } }