package de.uniluebeck.itm.wsn.drivers.core.operation;
import com.google.common.util.concurrent.TimeLimiter;
import com.google.common.util.concurrent.UncheckedTimeoutException;
import de.uniluebeck.itm.wsn.drivers.core.exception.TimeoutException;
import de.uniluebeck.itm.wsn.drivers.core.util.ClassUtil;
import org.apache.commons.lang3.event.EventListenerSupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* An abstract base class for {@link Operation} implementations.
*
* @param <ResultType>
* The return type of the operation.
*
* @author Malte Legenhausen
* @author Daniel Bimschas
*/
public abstract class TimeLimitedOperation<ResultType> implements Operation<ResultType> {
private final Logger log = LoggerFactory.getLogger(this.getClass());
/**
* Listeners for <code>OperationRunnable</code> changes.
*/
protected final EventListenerSupport<OperationListener<ResultType>> listeners =
EventListenerSupport.create(ClassUtil.<OperationListener<ResultType>>castClass(OperationListener.class));
/**
* Limiter for the execution time of an runnable.
*/
protected final TimeLimiter timeLimiter;
/**
* The timeout after which the application will be canceled.
*/
protected final long timeoutMillis;
/**
* A lock for controlling concurrent access to {@link TimeLimitedOperation#state}.
*/
protected final Lock stateLock = new ReentrantLock();
/**
* The current state of the <code>OperationRunnable</code>.
*/
protected State state = State.WAITING;
/**
* A condition that becomes true as soon as the operation is done.
*/
protected final Condition operationDone = stateLock.newCondition();
/**
* Boolean that stores if the operation has to be canceled.
*/
protected boolean canceled = false;
private float progress = 0f;
public TimeLimitedOperation(final TimeLimiter timeLimiter, final long timeoutMillis,
@Nullable final OperationListener<ResultType> listener) {
checkNotNull(timeLimiter);
checkArgument(timeoutMillis > 0, "Timeout must be larger than larger than zero milliseconds!");
this.timeLimiter = timeLimiter;
this.timeoutMillis = timeoutMillis;
if (listener != null) {
this.listeners.addListener(listener);
}
}
@Override
public void cancel() {
canceled = true;
stateLock.lock();
try {
operationDone.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
stateLock.unlock();
}
}
@Override
public final ResultType call() throws Exception {
setState(State.RUNNING);
listeners.fire().onExecute();
ResultType result = null;
try {
// Cancel execution if runnable was canceled before runnable changed to running.
if (!canceled) {
progress(0f);
log.trace("Running {} operation with {} ms timeout", this.getClass().getSimpleName(), timeoutMillis);
result = timeLimiter.callWithTimeout(new Callable<ResultType>() {
@Override
public ResultType call() throws Exception {
return callInternal();
}
}, timeoutMillis, TimeUnit.MILLISECONDS, false
);
progress(1f);
}
} catch (UncheckedTimeoutException e) {
setState(State.TIMEOUT);
TimeoutException timeoutException =
new TimeoutException("Operation timed out after " + timeoutMillis + " ms");
listeners.fire().onFailure(timeoutException);
throw timeoutException;
} catch (Exception e) {
setState(State.FAILED);
listeners.fire().onFailure(e);
throw e;
}
if (canceled) {
setState(State.CANCELED);
listeners.fire().onCancel();
result = null;
} else {
setState(State.DONE);
listeners.fire().onSuccess(result);
}
return result;
}
/**
* All operation execution code goes here. This method is call by {@link de.uniluebeck.itm.wsn.drivers.core.operation.TimeLimitedOperation#call()}
* which manages the operation state and notifies listeners about operation start and end.
*
* @return the result of the operation
*
* @throws Exception
* if an arbitrary exception occurs
*/
protected abstract ResultType callInternal() throws Exception;
@Override
public State getState() {
return state;
}
@Override
public long getTimeoutMillis() {
return timeoutMillis;
}
@Override
public void addListener(OperationListener<ResultType> listener) {
listeners.addListener(listener);
}
@Override
public void removeListener(OperationListener<ResultType> listener) {
listeners.removeListener(listener);
}
protected boolean isCanceled() {
return canceled;
}
protected <R> R runSubOperation(final Operation<R> subOperation, final float subFraction) throws Exception {
checkNotNull(subOperation, "Null operations are not allowed");
subOperation.addListener(new OperationAdapter<R>() {
private final float initialParentOperationProgress =
TimeLimitedOperation.this.progress;
@Override
public void onProgressChange(final float fraction) {
log.trace(
"Operation {}, progress: {}, suboperation {}, suboperation progress: {}",
TimeLimitedOperation.this.getClass().getSimpleName(),
TimeLimitedOperation.this.progress,
subOperation.getClass().getSimpleName(),
fraction
);
progress(initialParentOperationProgress + subFraction * fraction);
}
}
);
return subOperation.call();
}
/**
* Use this method to set the progress of work that was already done.
* The amount of work starts at 0.0f and goes up to 1.0f.
*
* @param progress
* The progress amount.
*/
protected void progress(float progress) {
log.trace("{} progress (old={}, new={})",
this.getClass().getSimpleName(), this.progress, progress
);
checkArgument(progress >= this.progress,
"A new progress value (%s) must be larger than the old value (%s). "
+ "It wouldn't be a progress otherwise, would it?", progress, this.progress
);
checkArgument(progress >= 0f && progress <= 1f, "Progress must be between zero and one (is %s).", progress);
this.progress = progress;
this.listeners.fire().onProgressChange(progress);
}
/**
* Thread safe state change function.
*
* @param newState
* The new State of this runnable.
*/
private void setState(State newState) {
stateLock.lock();
try {
State oldState = state;
fireBeforeStateChangedEvent(new StateChangedEvent<ResultType>(this, oldState, newState));
state = newState;
if (State.isFinishState(state)) {
operationDone.signalAll();
}
fireAfterStateChangedEvent(new StateChangedEvent<ResultType>(this, oldState, newState));
} finally {
stateLock.unlock();
}
}
private void fireBeforeStateChangedEvent(StateChangedEvent<ResultType> event) {
String msg = "{} state changing from {} to {}";
log.trace(msg, this.getClass().getSimpleName(), event.getOldState(), event.getNewState());
listeners.fire().beforeStateChanged(event);
}
/**
* Notify all listeners that the state has changed.
*
* @param event
* The state change event.
*/
private void fireAfterStateChangedEvent(StateChangedEvent<ResultType> event) {
String msg = "{} state changed from {} to {}";
log.trace(msg, this.getClass().getSimpleName(), event.getOldState(), event.getNewState());
listeners.fire().afterStateChanged(event);
}
}