/* * StreamCruncher: Copyright (c) 2006-2008, Ashwin Jayaprakash. All Rights Reserved. * Contact: ashwin {dot} jayaprakash {at} gmail {dot} com * Web: http://www.StreamCruncher.com * * This file is part of StreamCruncher. * * StreamCruncher 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 3 of the License, or * (at your option) any later version. * * StreamCruncher 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 StreamCruncher. If not, see <http://www.gnu.org/licenses/>. */ package streamcruncher.util; import java.io.Serializable; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; import streamcruncher.boot.Registry; /* * Author: Ashwin Jayaprakash Date: Jan 7, 2006 Time: 10:28:43 AM */ /** * This Class will work correctly when there is only one Producer Thread and one * Consumer Thread operating on it at any instant. There is no need for any * synchronization in such cases. */ public class AppendOnlyPrimitiveLongList implements Serializable { private static final long serialVersionUID = 1L; private static final long[] EMPTY_LONG_ARRAY = {}; public static final int FRAGMENT_SIZE = 512; public static final int WAIT_SCAN_TIME_MSECS = 300; protected final int fragmentSize; protected final AtomicInteger size; protected volatile Fragment head; protected volatile Fragment tail; /** * Uses a default fragment size of {@link #FRAGMENT_SIZE}. */ public AppendOnlyPrimitiveLongList() { this(FRAGMENT_SIZE); } public AppendOnlyPrimitiveLongList(int fragmentSize) { this.fragmentSize = fragmentSize; this.size = new AtomicInteger(0); this.head = createFragment(fragmentSize); this.tail = this.head; } // -------------------------- protected Fragment createFragment(int fragSize) { return new Fragment(fragSize); } // -------------------------- protected void moveHead() { Fragment newHead = head.getNext(); head.discard(); newHead.setPrev(null); head = newHead; } /** * @return Returns the number of items available. */ public int getSize() { return size.get(); } /** * @return Returns the fragmentSize. */ public int getFragmentSize() { return fragmentSize; } // -------------------------- public void add(long item) { if (tail.add(item) == false) { Fragment fragment = createFragment(fragmentSize); fragment.setPrev(tail); tail.setNext(fragment); tail = fragment; tail.add(item); } size.incrementAndGet(); } public void add(long[] items) { int copyStartPos = 0; while (copyStartPos < items.length) { int consumed = tail.add(items, copyStartPos); size.addAndGet(consumed); copyStartPos = copyStartPos + consumed; if (consumed == 0) { Fragment fragment = createFragment(fragmentSize); fragment.setPrev(tail); tail.setNext(fragment); tail = fragment; } } } /** * Non-blobking method call. * * @return <code>null</code> if there are no items available. Or, returns * the first item that can be read, This does not alter the state of * the "List". */ public Long peek() { if (size.get() == 0) { return null; } // -------------------------- Long retVal = null; try { retVal = head.peek(); } catch (NothingLeftToRemoveException e) { if (head != tail) { try { Fragment fragment = head.getNext(); retVal = fragment.peek(); } catch (NothingLeftToRemoveException e1) { // Can't happen. But log anyway. Logger logger = Registry.getImplFor(LoggerManager.class).getLogger( AppendOnlyPrimitiveLongList.class.getName()); logger.log(Level.WARNING, "Unexpected Exception: 'head.getNext().peek()'", e1); } } } return retVal; } /** * Uses default {@link AppendOnlyPrimitiveLongList#WAIT_SCAN_TIME_MSECS} * wait time when item is not available. * * @see #remove(long) * @return The first available item. */ public long remove() { return remove(WAIT_SCAN_TIME_MSECS); } /** * Threads keeps waiting until an item becomes available for removal. * * @param waitTimeMsecs * The time in milliseconds that the Thread must wait before * checking if an item is available for removal. * @return The first available item. */ public long remove(long waitTimeMsecs) { long retVal; try { retVal = head.remove(waitTimeMsecs); size.decrementAndGet(); } catch (NothingLeftToRemoveException e) { while (head == tail) { synchronized (head) { try { head.wait(waitTimeMsecs); } catch (InterruptedException e1) { Logger logger = Registry.getImplFor(LoggerManager.class).getLogger( AppendOnlyPrimitiveLongList.class.getName()); logger.log(Level.WARNING, e1.getMessage(), e1); } } } moveHead(); retVal = remove(waitTimeMsecs); } return retVal; } /** * This method returns an array of items currently available in the first * Fragment. Or, an empty-array if nothing is currently available. The size * of the array returned is smaller than the number indicated by * {@link #getSize()}. Since this Class maintains a list of Fragments, this * method returns all the contents available in the first Fragment in the * chain. Subsequent invocations will return the contents of the other * Fragments in-order in which they appear in the chain. * * @param maxLength * The maximum available items that must be returned. This * parameter cannot be greater than the Fragment-size returned by * {@link #getFragmentSize()}. * @return */ public long[] removeAvailable(int maxLength) { long[] retVal = EMPTY_LONG_ARRAY; try { retVal = head.removeAvailable(maxLength); } catch (NothingLeftToRemoveException e) { if (head != tail) { moveHead(); try { retVal = head.removeAvailable(maxLength); } catch (NothingLeftToRemoveException e1) { // Can't happen. throw new RuntimeException("Unexpected End-of-List", e1); } } } size.addAndGet(-retVal.length); return retVal; } /** * Returns the items available in the first Fragment. * * @return Maximum size returned will be equal to {@link #getFragmentSize()} * @see #removeAvailable(int) */ public long[] removeAvailable() { return removeAvailable(getFragmentSize()); } // -------------------------- protected static class NothingLeftToRemoveException extends Exception { private static final long serialVersionUID = 1L; } protected static class Fragment implements Serializable { private static final long serialVersionUID = 1L; private final Lock lock; private final Condition condition; /** * Since this exception keeps getting thrown over and over when the * Reader Thread keeps trying to remove all the items in the Chain of * Fragments, and not really for an Error condition, it's less expensive * to create it once and re-use it. */ protected final NothingLeftToRemoveException endMarker; protected volatile Fragment prev; protected final long[] data; protected volatile Fragment next; /** * Points to the position that has been read so far. -1, if nothing has * been read. */ protected volatile int readIndex; /** * Points to the first empty slot to which a new item has to be added. */ protected volatile int fillIndex; protected Fragment(int fragmentSize) { lock = new ReentrantLock(); condition = lock.newCondition(); endMarker = new NothingLeftToRemoveException(); StackTraceElement[] elements = endMarker.getStackTrace(); elements = new StackTraceElement[] { elements[0] }; endMarker.setStackTrace(elements); data = new long[fragmentSize]; readIndex = -1; fillIndex = 0; } // ---------------------- /** * @return Returns the next. */ public Fragment getNext() { return next; } /** * @param next * The next to set. */ public void setNext(Fragment next) { this.next = next; } /** * @return Returns the prev. */ public Fragment getPrev() { return prev; } /** * @param prev * The prev to set. */ public void setPrev(Fragment prev) { this.prev = prev; } // ---------------------- /** * @param item * @return <code>true</code> if Fragment has room to add this item, * <code>false</code> otherwise. */ public boolean add(long item) { if (fillIndex == data.length) { return false; } data[fillIndex] = item; fillIndex++; return true; } /** * @param items * @return The number of items that were copied from the Array provided. * <code>0</code> if the {@link Fragment} is full. */ public int add(long[] items, int startPos) { int toCopy = (data.length - fillIndex); int tmp = (items.length - startPos); toCopy = Math.min(toCopy, tmp); if (toCopy == 0) { return toCopy; } System.arraycopy(items, startPos, data, fillIndex, toCopy); fillIndex = fillIndex + toCopy; return toCopy; } /** * @return The first item available. Does not change the state. Just * peeks. * @throws NothingLeftToRemoveException * When this fragment has no items to remove. */ public long peek() throws NothingLeftToRemoveException { final int fillIdxSnapshot = fillIndex; final int readIdxSnapshot = readIndex; if (readIdxSnapshot == fillIdxSnapshot - 1) { if (fillIdxSnapshot == data.length) { throw endMarker; } } return data[readIdxSnapshot + 1]; } /** * @param spinWaitTimeMsecs * The time in Milliseconds to wait before checking again * (wait-check-wait loop) if an item is available for * removal. * @return Returns the first item that is available for removal. If * there is nothing currently available, then the Thread blocks * until an item is added. * @throws NothingLeftToRemoveException * If this Fragment has already been completely read and * removed. */ public long remove(long spinWaitTimeMsecs) throws NothingLeftToRemoveException { final int fillIdxSnapshot = fillIndex; if (readIndex == fillIdxSnapshot - 1) { if (fillIdxSnapshot == data.length) { throw endMarker; } lock.lock(); try { while (readIndex == fillIndex - 1) { try { condition.await(spinWaitTimeMsecs, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { Logger logger = Registry.getImplFor(LoggerManager.class).getLogger( AppendOnlyPrimitiveLongList.class.getName()); logger.log(Level.WARNING, e.getMessage(), e); } } } finally { lock.unlock(); } } return data[++readIndex]; } /** * @param maxLength * The maximum items that must be returned or less. Cannot * exceed the Fragment's size. * @return Returns an array containing the items that haven't been * removed before. Returns an empty-array if there is nothing * currently available to removal. * @throws NothingLeftToRemoveException * If this Fragment has already been completely read. */ public long[] removeAvailable(int maxLength) throws NothingLeftToRemoveException { final int fillIdxSnapshot = fillIndex; if (readIndex == fillIdxSnapshot - 1) { if (fillIdxSnapshot == data.length) { throw endMarker; } return EMPTY_LONG_ARRAY; } int resultLen = fillIdxSnapshot - readIndex - 1; long[] copy = null; if (resultLen == data.length && maxLength >= data.length) { // Reuse the whole array. copy = data; } else { resultLen = Math.min(resultLen, maxLength); copy = new long[resultLen]; System.arraycopy(data, readIndex + 1, copy, 0, copy.length); } readIndex += copy.length; return copy; } public void discard() { fillIndex = data.length; readIndex = data.length - 1; prev = null; next = null; } } }