package de.invesdwin.util.collections.iterable.concurrent; import java.util.NoSuchElementException; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.NotThreadSafe; import de.invesdwin.util.assertions.Assertions; import de.invesdwin.util.collections.iterable.ACloseableIterator; import de.invesdwin.util.collections.iterable.ICloseableIterator; import de.invesdwin.util.concurrent.Executors; import de.invesdwin.util.concurrent.WrappedExecutorService; import de.invesdwin.util.error.FastNoSuchElementException; @NotThreadSafe public class ProducerQueueIterator<E> extends ACloseableIterator<E> { private final class ProducerRunnable implements Runnable { @Override public void run() { try { while (!innerClosed && producer.hasNext()) { final E next = producer.next(); onElement(next); } } catch (final NoSuchElementException e) { innerClose(); } finally { //closing does not prevent queue from getting drained completely innerClose(); } } private void onElement(final E element) { try { Assertions.assertThat(element).isNotNull(); while (!innerClosed) { final boolean added = queue.offer(element); if (!added && queue.remainingCapacity() == 0) { if (utilizationDebugEnabled) { LOGGER.info(String.format("%s: queue is full", name)); } drainedLock.lock(); try { //wait till queue is drained again, start work immediately when a bit of space is free again while (!innerClosed && queue.size() >= queueSize) { drainedCondition.await(1, TimeUnit.SECONDS); } } finally { drainedLock.unlock(); } } if (added) { return; } } } catch (final InterruptedException e) { Thread.currentThread().interrupt(); innerClose(); } } } public static final int DEFAULT_QUEUE_SIZE = 10000; private static final org.slf4j.Logger LOGGER = org.slf4j.LoggerFactory.getLogger(ProducerQueueIterator.class); private final BlockingQueue<E> queue; private volatile boolean innerClosed; @GuardedBy("this") private E nextElement; private final Lock drainedLock = new ReentrantLock(); @GuardedBy("drainedLock") private final Condition drainedCondition = drainedLock.newCondition(); private final WrappedExecutorService executor; private ICloseableIterator<E> producer; private final String name; private final int queueSize; private boolean utilizationDebugEnabled; public ProducerQueueIterator(final String name, final ICloseableIterator<E> producer) { this(name, producer, DEFAULT_QUEUE_SIZE); } public ProducerQueueIterator(final String name, final ICloseableIterator<E> producer, final int queueSize) { this.producer = producer; this.queue = new LinkedBlockingDeque<E>(queueSize); this.name = name; this.queueSize = queueSize; this.executor = Executors.newFixedThreadPool(name, 1); this.executor.execute(new ProducerRunnable()); //read first element this.nextElement = readNext(); } public ProducerQueueIterator<E> withUtilizationDebugEnabled() { this.utilizationDebugEnabled = true; return this; } public boolean isUtilizationDebugEnabled() { return utilizationDebugEnabled; } @Override protected synchronized boolean innerHasNext() { final boolean hasNext = !innerClosed || !queue.isEmpty() || nextElement != null; if (!hasNext) { innerClose(); } return hasNext; } /* * always peek next and return current to prevent reaching end while being in next and thus having to return null or * throw NoSuchElementException without the caller expecting this */ @Override protected synchronized E innerNext() { if (hasNext()) { final E curElement = nextElement; nextElement = null; if (curElement == null) { throw new NullPointerException("should not happen, since hasNext was called!"); } nextElement = readNext(); return curElement; } else { throw new FastNoSuchElementException("ProducerQueueIterator: hasNext is false"); } } private E readNext() { try { boolean firstPoll = true; while (hasNext()) { if (!firstPoll && utilizationDebugEnabled) { LOGGER.info(String.format("%s: queue is empty", name)); } firstPoll = false; final E element = queue.poll(1, TimeUnit.SECONDS); if (element != null) { drainedLock.lock(); try { drainedCondition.signalAll(); } finally { drainedLock.unlock(); } return element; } } } catch (final InterruptedException e) { Thread.currentThread().interrupt(); return null; } return null; } @Override protected void innerClose() { if (!innerClosed) { innerClosed = true; executor.shutdown(); //cannot wait here for executor to close completely since the thread could trigger it himself producer.close(); } } }