Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.gh.bmd.jrt.core.DefaultResultChannel Maven / Gradle / Ivy
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.gh.bmd.jrt.core;
import com.gh.bmd.jrt.builder.OutputDeadlockException;
import com.gh.bmd.jrt.builder.RoutineConfiguration;
import com.gh.bmd.jrt.builder.RoutineConfiguration.OrderType;
import com.gh.bmd.jrt.builder.RoutineConfiguration.TimeoutActionType;
import com.gh.bmd.jrt.channel.OutputChannel;
import com.gh.bmd.jrt.channel.OutputConsumer;
import com.gh.bmd.jrt.channel.ReadDeadlockException;
import com.gh.bmd.jrt.channel.ResultChannel;
import com.gh.bmd.jrt.common.AbortException;
import com.gh.bmd.jrt.common.InvocationInterruptedException;
import com.gh.bmd.jrt.common.WeakIdentityHashMap;
import com.gh.bmd.jrt.log.Logger;
import com.gh.bmd.jrt.runner.Execution;
import com.gh.bmd.jrt.runner.Runner;
import com.gh.bmd.jrt.time.TimeDuration;
import com.gh.bmd.jrt.time.TimeDuration.Check;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import static com.gh.bmd.jrt.time.TimeDuration.INFINITY;
import static com.gh.bmd.jrt.time.TimeDuration.ZERO;
import static com.gh.bmd.jrt.time.TimeDuration.fromUnit;
/**
* Class handling the routine output.
*
* This class centralizes the managing of data passing through the routine output and result
* channels, since, logically, the two objects are part of the same entity. In fact, on one end the
* result channel puts data into the output queue and, on the other end, the output channel reads
* them from the same queue.
*
* Created by davide on 9/24/14.
*
* @param the output data type.
*/
class DefaultResultChannel implements ResultChannel {
private static final WeakIdentityHashMap, Object> sMutexMap =
new WeakIdentityHashMap, Object>();
private final ArrayList> mBoundChannels = new ArrayList>();
private final Object mFlushMutex = new Object();
private final AbortHandler mHandler;
private final Check mHasOutputs;
private final Logger mLogger;
private final int mMaxOutput;
private final Object mMutex = new Object();
private final TimeDuration mOutputTimeout;
private final TimeDuration mReadTimeout;
private final Runner mRunner;
private final TimeoutActionType mTimeoutActionType;
private Throwable mAbortException;
private Object mConsumerMutex;
private boolean mIsException;
private OutputConsumer super OUTPUT> mOutputConsumer;
private int mOutputCount;
private Check mOutputHasNext;
private Check mOutputNotEmpty;
private NestedQueue mOutputQueue;
private int mPendingOutputCount;
private TimeDuration mResultDelay = ZERO;
private ChannelState mState = ChannelState.OUTPUT;
/**
* Constructor.
*
* @param configuration the routine configuration.
* @param handler the abort handler.
* @param runner the runner instance.
* @param logger the logger instance.
*/
@SuppressWarnings("ConstantConditions")
DefaultResultChannel(@Nonnull final RoutineConfiguration configuration,
@Nonnull final AbortHandler handler, @Nonnull final Runner runner,
@Nonnull final Logger logger) {
if (handler == null) {
throw new NullPointerException("the abort handler must not be null");
}
if (runner == null) {
throw new NullPointerException("the runner instance must not be null");
}
mLogger = logger.subContextLogger(this);
mHandler = handler;
mRunner = runner;
mReadTimeout = configuration.getReadTimeoutOr(ZERO);
mTimeoutActionType = configuration.getReadTimeoutActionOr(TimeoutActionType.DEADLOCK);
mMaxOutput = configuration.getOutputMaxSizeOr(Integer.MAX_VALUE);
mOutputTimeout = configuration.getOutputTimeoutOr(ZERO);
mOutputQueue = (configuration.getOutputOrderTypeOr(OrderType.NONE) == OrderType.NONE)
? new SimpleNestedQueue() : new OrderedNestedQueue();
final int maxOutputSize = mMaxOutput;
mHasOutputs = new Check() {
public boolean isTrue() {
return (mOutputCount <= maxOutputSize);
}
};
}
@Nonnull
private static Object getMutex(@Nonnull final OutputConsumer> consumer) {
synchronized (sMutexMap) {
final WeakIdentityHashMap, Object> mutexMap = sMutexMap;
Object mutex = mutexMap.get(consumer);
if (mutex == null) {
mutex = new Object();
mutexMap.put(consumer, mutex);
}
return mutex;
}
}
public boolean abort() {
return abort(null);
}
@Nonnull
@SuppressWarnings("ConstantConditions")
public ResultChannel after(@Nonnull final TimeDuration delay) {
synchronized (mMutex) {
verifyOutput();
if (delay == null) {
mLogger.err("invalid null delay");
throw new NullPointerException("the input delay must not be null");
}
mResultDelay = delay;
}
return this;
}
@Nonnull
public ResultChannel after(final long delay, @Nonnull final TimeUnit timeUnit) {
return after(fromUnit(delay, timeUnit));
}
@Nonnull
public ResultChannel now() {
return after(ZERO);
}
@Nonnull
public ResultChannel pass(@Nullable final OutputChannel extends OUTPUT> channel) {
final TimeDuration delay;
final DefaultOutputConsumer consumer;
synchronized (mMutex) {
verifyOutput();
if (channel == null) {
mLogger.wrn("passing null channel");
return this;
}
mBoundChannels.add(channel);
delay = mResultDelay;
++mPendingOutputCount;
mLogger.dbg("passing channel: %s", channel);
consumer = new DefaultOutputConsumer(delay);
}
channel.bind(consumer);
return this;
}
@Nonnull
public ResultChannel pass(@Nullable final Iterable extends OUTPUT> outputs) {
NestedQueue outputQueue;
ArrayList list = null;
final TimeDuration delay;
synchronized (mMutex) {
verifyOutput();
if (outputs == null) {
mLogger.wrn("passing null iterable");
return this;
}
outputQueue = mOutputQueue;
delay = mResultDelay;
int count = 0;
if (delay.isZero()) {
for (final OUTPUT output : outputs) {
outputQueue.add(output);
++count;
}
} else {
outputQueue = outputQueue.addNested();
list = new ArrayList();
for (final OUTPUT output : outputs) {
list.add(output);
}
count = list.size();
++mPendingOutputCount;
}
mLogger.dbg("passing iterable [#%d+%d]: %s [%s]", mOutputCount, count, outputs, delay);
addOutputs(count);
}
if (delay.isZero()) {
flushOutput(false);
} else {
mRunner.run(new DelayedListOutputExecution(outputQueue, list), delay.time, delay.unit);
}
return this;
}
@Nonnull
public ResultChannel pass(@Nullable final OUTPUT output) {
NestedQueue outputQueue;
final TimeDuration delay;
synchronized (mMutex) {
verifyOutput();
outputQueue = mOutputQueue;
delay = mResultDelay;
if (delay.isZero()) {
outputQueue.add(output);
} else {
outputQueue = outputQueue.addNested();
++mPendingOutputCount;
}
mLogger.dbg("passing output [#%d+1]: %s [%s]", mOutputCount, output, delay);
addOutputs(1);
}
if (delay.isZero()) {
flushOutput(false);
} else {
mRunner.run(new DelayedOutputExecution(outputQueue, output), delay.time, delay.unit);
}
return this;
}
@Nonnull
public ResultChannel pass(@Nullable final OUTPUT... outputs) {
synchronized (mMutex) {
verifyOutput();
if (outputs == null) {
mLogger.wrn("passing null output array");
return this;
}
}
return pass(Arrays.asList(outputs));
}
/**
* Aborts immediately the execution.
*
* @param throwable the reason of the abortion.
* @see com.gh.bmd.jrt.channel.Channel#abort(Throwable)
*/
void abortImmediately(@Nullable final Throwable throwable) {
abort(throwable, true);
}
/**
* Closes this channel with the specified exception.
*
* @param throwable the exception.
*/
void close(@Nullable final Throwable throwable) {
final ArrayList> channels;
synchronized (mMutex) {
mLogger.dbg(throwable, "aborting result channel");
channels = new ArrayList>(mBoundChannels);
mBoundChannels.clear();
mOutputQueue.add(RoutineExceptionWrapper.wrap(throwable));
mIsException = true;
if (mAbortException == null) {
mAbortException = throwable;
}
mState = ChannelState.ABORTED;
mMutex.notifyAll();
}
for (final OutputChannel> channel : channels) {
channel.abort(throwable);
}
flushOutput(false);
}
/**
* Closes this channel successfully.
*/
void close() {
boolean isFlush = false;
synchronized (mMutex) {
mLogger.dbg("closing result channel [#%d]", mPendingOutputCount);
if (mState == ChannelState.OUTPUT) {
isFlush = true;
if (mPendingOutputCount > 0) {
mState = ChannelState.RESULT;
} else {
mState = ChannelState.FLUSH;
}
} else {
mLogger.dbg("avoiding closing result channel since already closed");
}
}
if (isFlush) {
flushOutput(false);
}
}
/**
* Returns the output channel reading the data pushed into this channel.
*
* @return the output channel.
*/
@Nonnull
OutputChannel getOutput() {
final TimeoutActionType action = mTimeoutActionType;
final OutputChannel outputChannel =
new DefaultOutputChannel().afterMax(mReadTimeout);
if (action == TimeoutActionType.EXIT) {
outputChannel.eventuallyExit();
} else if (action == TimeoutActionType.ABORT) {
outputChannel.eventuallyAbort();
}
return outputChannel;
}
private boolean abort(@Nullable final Throwable throwable, final boolean isImmediate) {
final TimeDuration delay;
synchronized (mMutex) {
if (isResultComplete()) {
mLogger.dbg(throwable, "avoiding aborting since channel is closed");
return false;
}
delay = (isImmediate) ? ZERO : mResultDelay;
if (delay.isZero()) {
mLogger.dbg(throwable, "aborting channel");
mOutputQueue.clear();
mIsException = true;
mAbortException = (isImmediate || (throwable instanceof AbortException)) ? throwable
: new AbortException(throwable);
mState = ChannelState.EXCEPTION;
}
}
if (delay.isZero()) {
mHandler.onAbort(throwable, 0, TimeUnit.MILLISECONDS);
} else {
mRunner.run(new DelayedAbortExecution(throwable), delay.time, delay.unit);
}
return true;
}
private void addOutputs(final int count) {
mOutputCount += count;
try {
if (!mOutputTimeout.waitTrue(mMutex, mHasOutputs)) {
throw new OutputDeadlockException(
"deadlock while waiting for room in the output channel");
}
} catch (final InterruptedException e) {
throw new InvocationInterruptedException(e);
}
}
@SuppressFBWarnings(value = "UWF_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR",
justification = "cannot be called if output consumer is null")
private void closeConsumer(final ChannelState state) {
if (state != ChannelState.ABORTED) {
final Logger logger = mLogger;
final OutputConsumer super OUTPUT> consumer = mOutputConsumer;
try {
logger.dbg("closing consumer (%s)", consumer);
consumer.onComplete();
} catch (final InvocationInterruptedException e) {
throw e;
} catch (final Throwable ignored) {
logger.wrn(ignored, "ignoring consumer exception (%s)", consumer);
}
}
synchronized (mMutex) {
if (!isOutputPending(mState)) {
mState = ChannelState.DONE;
mMutex.notifyAll();
}
}
}
@SuppressWarnings({"SynchronizeOnNonFinalField", "unchecked"})
private void flushOutput(final boolean forceClose) {
Throwable abortException = null;
synchronized (mFlushMutex) {
final Logger logger = mLogger;
final ArrayList outputs;
final OutputConsumer super OUTPUT> consumer;
final ChannelState state;
synchronized (mMutex) {
consumer = mOutputConsumer;
if (consumer == null) {
logger.dbg("avoiding flushing output since channel is not bound");
if (!isOutputPending(mState)) {
mState = ChannelState.DONE;
}
mMutex.notifyAll();
return;
}
outputs = new ArrayList();
mOutputQueue.moveTo(outputs);
state = mState;
mOutputCount = 0;
mMutex.notifyAll();
}
synchronized (mConsumerMutex) {
try {
for (final Object output : outputs) {
if (output instanceof RoutineExceptionWrapper) {
try {
logger.dbg("aborting consumer (%s): %s", consumer, output);
consumer.onError(((RoutineExceptionWrapper) output).getCause());
} catch (final InvocationInterruptedException e) {
throw e;
} catch (final Throwable ignored) {
logger.wrn(ignored, "ignoring consumer exception (%s)", consumer);
}
break;
} else {
logger.dbg("output consumer (%s): %s", consumer, output);
consumer.onOutput((OUTPUT) output);
}
}
if (forceClose || !isOutputPending(state)) {
closeConsumer(state);
}
} catch (final InvocationInterruptedException e) {
throw e;
} catch (final Throwable t) {
boolean isClose = false;
final ChannelState finalState;
synchronized (mMutex) {
logger.wrn(t, "consumer exception (%s)", mOutputConsumer);
finalState = mState;
if (forceClose || !isOutputPending(finalState)) {
isClose = true;
} else if (finalState != ChannelState.EXCEPTION) {
logger.wrn(t, "aborting on consumer exception (%s)", mOutputConsumer);
abortException = t;
mOutputQueue.clear();
mIsException = true;
mAbortException = t;
mState = ChannelState.EXCEPTION;
}
}
if (isClose) {
closeConsumer(finalState);
}
}
}
}
if (abortException != null) {
mHandler.onAbort(abortException, 0, TimeUnit.MILLISECONDS);
}
}
private boolean isOutputOpen() {
synchronized (mMutex) {
return !mOutputQueue.isEmpty() || (mState != ChannelState.DONE);
}
}
private boolean isOutputPending(@Nonnull final ChannelState state) {
return (state != ChannelState.FLUSH) && (state != ChannelState.ABORTED);
}
private boolean isResultComplete() {
return (mState.ordinal() >= ChannelState.FLUSH.ordinal());
}
private boolean isResultOpen() {
synchronized (mMutex) {
return (mState == ChannelState.OUTPUT);
}
}
@Nullable
@SuppressWarnings("unchecked")
private OUTPUT nextOutput(@Nonnull final TimeDuration timeout) {
final Object result = mOutputQueue.removeFirst();
mLogger.dbg("reading output [#%d]: %s [%s]", mOutputCount, result, timeout);
RoutineExceptionWrapper.raise(result);
final int maxOutput = mMaxOutput;
final int prevOutputCount = mOutputCount;
if ((--mOutputCount < maxOutput) && (prevOutputCount >= maxOutput)) {
mMutex.notifyAll();
}
return (OUTPUT) result;
}
@Nullable
private OUTPUT readQueue(@Nonnull final TimeDuration timeout,
@Nonnull final TimeoutActionType action) {
verifyBound();
final Logger logger = mLogger;
final NestedQueue outputQueue = mOutputQueue;
if (timeout.isZero() || !outputQueue.isEmpty()) {
if (outputQueue.isEmpty()) {
logger.wrn("reading output timeout: [%s] => [%s]", timeout, action);
if (action == TimeoutActionType.DEADLOCK) {
throw new ReadDeadlockException("deadlock while waiting for outputs");
}
}
return nextOutput(timeout);
}
if (mOutputNotEmpty == null) {
mOutputNotEmpty = new Check() {
public boolean isTrue() {
return !outputQueue.isEmpty();
}
};
}
final boolean isTimeout;
try {
isTimeout = !timeout.waitTrue(mMutex, mOutputNotEmpty);
} catch (final InterruptedException e) {
throw new InvocationInterruptedException(e);
}
if (isTimeout) {
logger.wrn("reading output timeout: [%s] => [%s]", timeout, action);
if (action == TimeoutActionType.DEADLOCK) {
throw new ReadDeadlockException("deadlock while waiting for outputs");
}
}
return nextOutput(timeout);
}
private void verifyBound() {
if (mOutputConsumer != null) {
mLogger.err("invalid call on bound channel");
throw new IllegalStateException("the channel is already bound");
}
}
private void verifyOutput() {
if (mIsException) {
final Throwable throwable = mAbortException;
mLogger.dbg(throwable, "abort exception");
throw RoutineExceptionWrapper.wrap(throwable).raise();
}
if (!isResultOpen()) {
mLogger.err("invalid call on closed channel");
throw new IllegalStateException("the channel is closed");
}
}
/**
* Enumeration identifying the channel internal state.
*/
private enum ChannelState {
OUTPUT, // result channel is open
RESULT, // result channel is closed
FLUSH, // no more pending outputs
EXCEPTION, // abort issued
ABORTED, // invocation aborted
DONE // output is closed
}
/**
* Interface defining an abort handler.
*/
public interface AbortHandler {
/**
* Called on an abort.
*
* @param reason the reason of the abortion.
* @param delay the abortion delay.
* @param timeUnit the delay time unit.
*/
void onAbort(@Nullable Throwable reason, long delay, @Nonnull TimeUnit timeUnit);
}
/**
* Default implementation of an output channel iterator.
*/
private class DefaultIterator implements Iterator {
private final TimeoutActionType mAction;
private final Logger mSubLogger = mLogger.subContextLogger(this);
private final TimeDuration mTimeout;
private boolean mRemoved = true;
/**
* Constructor.
*
* @param timeout the output timeout.
* @param action the timeout action.
*/
private DefaultIterator(@Nonnull final TimeDuration timeout,
@Nonnull final TimeoutActionType action) {
mTimeout = timeout;
mAction = action;
}
public boolean hasNext() {
boolean isAbort = false;
synchronized (mMutex) {
verifyBound();
final Logger logger = mSubLogger;
final TimeDuration timeout = mTimeout;
final NestedQueue outputQueue = mOutputQueue;
if (timeout.isZero() || (mState == ChannelState.DONE)) {
final boolean hasNext = !outputQueue.isEmpty();
if (!hasNext && (mState != ChannelState.DONE)) {
final TimeoutActionType action = mAction;
logger.wrn("has output timeout: [%s] => [%s]", timeout, action);
if (action == TimeoutActionType.DEADLOCK) {
throw new ReadDeadlockException(
"deadlock while waiting to know if more outputs are coming");
} else {
isAbort = (action == TimeoutActionType.ABORT);
}
}
} else {
if (mOutputHasNext == null) {
mOutputHasNext = new Check() {
public boolean isTrue() {
return !outputQueue.isEmpty() || (mState == ChannelState.DONE);
}
};
}
final boolean isTimeout;
try {
isTimeout = !timeout.waitTrue(mMutex, mOutputHasNext);
} catch (final InterruptedException e) {
throw new InvocationInterruptedException(e);
}
if (isTimeout) {
final TimeoutActionType action = mAction;
logger.wrn("has output timeout: [%s] => [%s]", timeout, action);
if (action == TimeoutActionType.DEADLOCK) {
throw new ReadDeadlockException(
"deadlock while waiting to know if more outputs are coming");
} else {
isAbort = (action == TimeoutActionType.ABORT);
}
}
}
if (!isAbort) {
final boolean hasNext = !outputQueue.isEmpty();
logger.dbg("has output: %s [%s]", hasNext, timeout);
return hasNext;
}
}
abort();
throw new AbortException(null);
}
@Nullable
@SuppressFBWarnings(value = "IT_NO_SUCH_ELEMENT",
justification = "NestedQueue.removeFirst() actually throws it")
public OUTPUT next() {
boolean isAbort = false;
try {
synchronized (mMutex) {
final TimeoutActionType action = mAction;
isAbort = (action == TimeoutActionType.ABORT);
final OUTPUT next = readQueue(mTimeout, action);
mRemoved = false;
return next;
}
} catch (final NoSuchElementException e) {
if (isAbort) {
abort();
throw new AbortException(null);
}
throw e;
}
}
public void remove() {
synchronized (mMutex) {
verifyBound();
if (mRemoved) {
mSubLogger.err("invalid output remove");
throw new IllegalStateException("the element has been already removed");
}
mRemoved = true;
}
}
}
/**
* Default implementation of a routine output channel.
*/
private class DefaultOutputChannel implements OutputChannel {
private final Logger mSubLogger = mLogger.subContextLogger(this);
private TimeDuration mReadTimeout = ZERO;
private TimeoutActionType mTimeoutActionType = TimeoutActionType.DEADLOCK;
@Nonnull
@SuppressWarnings("ConstantConditions")
public OutputChannel afterMax(@Nonnull final TimeDuration timeout) {
synchronized (mMutex) {
if (timeout == null) {
mSubLogger.err("invalid null timeout");
throw new NullPointerException("the output timeout must not be null");
}
mReadTimeout = timeout;
}
return this;
}
@Nonnull
public OutputChannel afterMax(final long timeout,
@Nonnull final TimeUnit timeUnit) {
return afterMax(fromUnit(timeout, timeUnit));
}
@Nonnull
@SuppressWarnings("ConstantConditions")
public OutputChannel bind(@Nonnull final OutputConsumer super OUTPUT> consumer) {
final boolean forceClose;
final ChannelState state;
synchronized (mMutex) {
verifyBound();
if (consumer == null) {
mSubLogger.err("invalid null consumer");
throw new NullPointerException("the output consumer must not be null");
}
state = mState;
forceClose = (state == ChannelState.DONE);
mOutputConsumer = consumer;
mConsumerMutex = getMutex(consumer);
}
flushOutput(forceClose);
return this;
}
public boolean checkComplete() {
final boolean isDone;
synchronized (mMutex) {
final TimeDuration timeout = mReadTimeout;
try {
isDone = timeout.waitTrue(mMutex, new Check() {
public boolean isTrue() {
return (mState == ChannelState.DONE);
}
});
} catch (final InterruptedException e) {
throw new InvocationInterruptedException(e);
}
if (!isDone) {
mSubLogger.wrn("waiting complete timeout: [%s]", timeout);
}
}
return isDone;
}
@Nonnull
public OutputChannel eventually() {
return afterMax(INFINITY);
}
@Nonnull
public OutputChannel eventuallyAbort() {
synchronized (mMutex) {
mTimeoutActionType = TimeoutActionType.ABORT;
}
return this;
}
@Nonnull
public OutputChannel eventuallyDeadlock() {
synchronized (mMutex) {
mTimeoutActionType = TimeoutActionType.DEADLOCK;
}
return this;
}
@Nonnull
public OutputChannel eventuallyExit() {
synchronized (mMutex) {
mTimeoutActionType = TimeoutActionType.EXIT;
}
return this;
}
@Nonnull
public OutputChannel immediately() {
return afterMax(ZERO);
}
public boolean isBound() {
synchronized (mMutex) {
return (mOutputConsumer != null);
}
}
@Nonnull
public List readAll() {
final ArrayList results = new ArrayList();
readAllInto(results);
return results;
}
@Nonnull
@SuppressWarnings({"unchecked", "ConstantConditions"})
public OutputChannel readAllInto(
@Nonnull final Collection super OUTPUT> results) {
boolean isAbort = false;
synchronized (mMutex) {
verifyBound();
final Logger logger = mSubLogger;
if (results == null) {
logger.err("invalid null output list");
throw new NullPointerException("the result list must not be null");
}
final NestedQueue outputQueue = mOutputQueue;
final TimeDuration timeout = mReadTimeout;
if (timeout.isZero() || (mState == ChannelState.DONE)) {
while (!outputQueue.isEmpty()) {
final OUTPUT result = nextOutput(timeout);
logger.dbg("adding output to list: %s [%s]", result, timeout);
results.add(result);
}
if (mState != ChannelState.DONE) {
final TimeoutActionType action = mTimeoutActionType;
logger.wrn("list output timeout: [%s] => [%s]", timeout, action);
if (action == TimeoutActionType.DEADLOCK) {
throw new ReadDeadlockException(
"deadlock while waiting to collect all outputs");
} else {
isAbort = (action == TimeoutActionType.ABORT);
}
}
} else {
final long startTime = System.currentTimeMillis();
final boolean isTimeout;
try {
do {
while (!outputQueue.isEmpty()) {
final OUTPUT result = nextOutput(timeout);
logger.dbg("adding output to list: %s [%s]", result, timeout);
results.add(result);
}
if (mState == ChannelState.DONE) {
break;
}
} while (timeout.waitSinceMillis(mMutex, startTime));
isTimeout = (mState != ChannelState.DONE);
} catch (final InterruptedException e) {
throw new InvocationInterruptedException(e);
}
if (isTimeout) {
final TimeoutActionType action = mTimeoutActionType;
logger.wrn("list output timeout: [%s] => [%s]", timeout, action);
if (action == TimeoutActionType.DEADLOCK) {
throw new ReadDeadlockException(
"deadlock while waiting to collect all outputs");
} else {
isAbort = (action == TimeoutActionType.ABORT);
}
}
}
}
if (isAbort) {
abort();
throw new AbortException(null);
}
return this;
}
public OUTPUT readNext() {
boolean isAbort = false;
try {
synchronized (mMutex) {
final TimeoutActionType action = mTimeoutActionType;
isAbort = (action == TimeoutActionType.ABORT);
return readQueue(mReadTimeout, action);
}
} catch (final NoSuchElementException e) {
if (isAbort) {
abort();
throw new AbortException(null);
}
throw e;
}
}
@Nonnull
public OutputChannel unbind(
@Nullable final OutputConsumer super OUTPUT> consumer) {
synchronized (mMutex) {
if (mOutputConsumer == consumer) {
mOutputConsumer = null;
mConsumerMutex = null;
}
}
return this;
}
@Nonnull
public Iterator iterator() {
final TimeDuration timeout;
final TimeoutActionType action;
synchronized (mMutex) {
verifyBound();
timeout = mReadTimeout;
action = mTimeoutActionType;
}
return new DefaultIterator(timeout, action);
}
public boolean abort() {
return abort(null);
}
public boolean abort(@Nullable final Throwable reason) {
synchronized (mMutex) {
if (isResultComplete()) {
mSubLogger.dbg("avoiding aborting output since result channel is closed");
return false;
}
mSubLogger.dbg(reason, "aborting output");
mOutputQueue.clear();
mIsException = true;
mAbortException =
(reason instanceof AbortException) ? reason : new AbortException(reason);
mState = ChannelState.EXCEPTION;
}
mHandler.onAbort(reason, 0, TimeUnit.MILLISECONDS);
return true;
}
public boolean isOpen() {
return isOutputOpen();
}
}
/**
* Default implementation of an output consumer pushing the data to consume into the output
* channel queue.
*/
private class DefaultOutputConsumer implements OutputConsumer {
private final TimeDuration mDelay;
private final NestedQueue mQueue;
private final Logger mSubLogger = mLogger.subContextLogger(this);
/**
* Constructor.
*
* @param delay the output delay.
*/
private DefaultOutputConsumer(@Nonnull final TimeDuration delay) {
mDelay = delay;
mQueue = mOutputQueue.addNested();
}
public void onComplete() {
boolean isFlush = false;
synchronized (mMutex) {
verifyComplete();
mQueue.close();
if ((--mPendingOutputCount == 0) && (mState == ChannelState.RESULT)) {
mState = ChannelState.FLUSH;
isFlush = true;
} else {
mMutex.notifyAll();
}
mSubLogger.dbg("closing output [%s]", isFlush);
}
if (isFlush) {
flushOutput(false);
}
}
public void onError(@Nullable final Throwable error) {
synchronized (mMutex) {
if (isResultComplete()) {
mSubLogger.dbg("avoiding aborting output since result channel is closed");
return;
}
mSubLogger.dbg(error, "aborting output");
mOutputQueue.clear();
mIsException = true;
mAbortException = error;
mState = ChannelState.EXCEPTION;
}
final TimeDuration delay = mDelay;
mHandler.onAbort(error, delay.time, delay.unit);
}
public void onOutput(final OUTPUT output) {
NestedQueue outputQueue;
final TimeDuration delay = mDelay;
synchronized (mMutex) {
verifyComplete();
outputQueue = mQueue;
if (delay.isZero()) {
outputQueue.add(output);
} else {
outputQueue = outputQueue.addNested();
++mPendingOutputCount;
}
mSubLogger.dbg("consumer output [#%d+1]: %s [%s]", mOutputCount, output, delay);
addOutputs(1);
}
if (delay.isZero()) {
flushOutput(false);
} else {
mRunner.run(new DelayedOutputExecution(outputQueue, output), delay.time,
delay.unit);
}
}
private void verifyComplete() {
if (mIsException) {
final Throwable throwable = mAbortException;
mSubLogger.dbg(throwable, "consumer abort exception");
throw RoutineExceptionWrapper.wrap(throwable).raise();
}
if (isResultComplete()) {
mSubLogger.err("consumer invalid call on closed channel");
throw new IllegalStateException("the channel is closed");
}
}
}
/**
* Implementation of an execution handling a delayed abort.
*/
private class DelayedAbortExecution implements Execution {
private final Throwable mThrowable;
/**
* Constructor.
*
* @param throwable the reason of the abort.
*/
private DelayedAbortExecution(@Nullable final Throwable throwable) {
mThrowable = throwable;
}
public void run() {
final Throwable throwable = mThrowable;
synchronized (mMutex) {
if (!isOutputOpen()) {
mLogger.dbg(throwable, "avoiding aborting since channel is closed");
return;
}
mLogger.dbg(throwable, "aborting channel");
mOutputQueue.clear();
mIsException = true;
mAbortException = throwable;
mState = ChannelState.EXCEPTION;
}
mHandler.onAbort(throwable, 0, TimeUnit.MILLISECONDS);
}
}
/**
* Implementation of an execution handling a delayed output of a list of data.
*/
private class DelayedListOutputExecution implements Execution {
private final ArrayList mOutputs;
private final NestedQueue mQueue;
/**
* Constructor.
*
* @param queue the output queue.
* @param outputs the list of output data.
*/
private DelayedListOutputExecution(@Nonnull final NestedQueue queue,
final ArrayList outputs) {
mOutputs = outputs;
mQueue = queue;
}
public void run() {
synchronized (mMutex) {
if (isResultComplete()) {
mLogger.dbg("avoiding delayed output execution since channel is closed: %s",
mOutputs);
return;
}
mLogger.dbg("delayed output execution: %s", mOutputs);
if ((--mPendingOutputCount == 0) && (mState == ChannelState.RESULT)) {
mState = ChannelState.FLUSH;
}
final NestedQueue queue = mQueue;
queue.addAll(mOutputs);
queue.close();
}
flushOutput(false);
}
}
/**
* Implementation of an execution handling a delayed output.
*/
private class DelayedOutputExecution implements Execution {
private final OUTPUT mOutput;
private final NestedQueue mQueue;
/**
* Constructor.
*
* @param queue the output queue.
* @param output the output.
*/
private DelayedOutputExecution(@Nonnull final NestedQueue queue,
@Nullable final OUTPUT output) {
mQueue = queue;
mOutput = output;
}
public void run() {
synchronized (mMutex) {
if (isResultComplete()) {
mLogger.dbg("avoiding delayed output execution since channel is closed: %s",
mOutput);
return;
}
mLogger.dbg("delayed output execution: %s", mOutput);
if ((--mPendingOutputCount == 0) && (mState == ChannelState.RESULT)) {
mState = ChannelState.FLUSH;
}
final NestedQueue queue = mQueue;
queue.add(mOutput);
queue.close();
}
flushOutput(false);
}
}
public boolean abort(@Nullable final Throwable reason) {
return abort(reason, false);
}
public boolean isOpen() {
return isResultOpen();
}
}