/*
This file is part of Reactive Cascade which is released under The MIT License.
See license.md , https://github.com/futurice/cascade and http://reactivecascade.com for details.
This is open source for the common good. Please contribute improvements by pull request or contact paulirotta@gmail.com
*/
package com.reactivecascade.util;
import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import com.reactivecascade.Async;
import com.reactivecascade.BuildConfig;
import com.reactivecascade.functional.ImmutableValue;
import com.reactivecascade.functional.RunnableAltFuture;
import com.reactivecascade.functional.SettableAltFuture;
import com.reactivecascade.i.IAction;
import com.reactivecascade.i.IActionOne;
import com.reactivecascade.i.IActionOneR;
import com.reactivecascade.i.IActionR;
import com.reactivecascade.i.IAltFuture;
import com.reactivecascade.i.IRunnableAltFuture;
import com.reactivecascade.i.ISettableAltFuture;
import com.reactivecascade.i.IThreadType;
import com.reactivecascade.i.NotCallOrigin;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
/**
* The baseline implementation of ThreadType convenience classes. It provides functional interfaces
* (lambda-friendly if you are using the RetroLambda library or similar) to execRunnable code in a background
* WORKER thread pool.
* <p>
* For more specialized behaviour a class may choose to replace this.
* <p>
*/
public abstract class AbstractThreadType extends Origin implements IThreadType {
@NonNull
protected final ExecutorService executorService;
@Nullable
protected final BlockingQueue<Runnable> queue;
@NonNull
private final String name;
/**
* Create an asynchronous mOnFireAction handler that embodies certain rules for threading split concurrency
* in a set of lambda-friendly methods
*
* @param executorService the thread or thread pool for this thread type. Threads and thread pools may be
* shared with other thread types, however note that though this this cooperative execution
* reduces context switching and peak memory load it may delay the start of execution
* of tasks in one thread type by tasks in another thread type
*/
public AbstractThreadType(@NonNull String name,
@NonNull ExecutorService executorService,
@Nullable BlockingQueue<Runnable> queue) {
this.name = name;
this.executorService = executorService;
this.queue = queue;
}
//============================= Internal Utility Methods =========================================
private static boolean isMistakenlyCalledDirectlyFromOutsideTheCascadeLibrary() {
//TODO This check doesn't really allow 3rd party implementations. Not testing would mean unsafe/less obvious problems can come later. Package hiding would disallow replacement implementations that follow the interface contracts. What we have here is a half measure to guide people since currently there are no alternate implementations.
StackTraceElement[] ste = Thread.currentThread().getStackTrace();
AssertUtil.assertTrue("Stack trace[3] is AbstractThreadType.fork(IRunnableAltFuture)", ste[3].getMethodName().contains("fork"));
return !ste[4].getClassName().startsWith("com.reactivecascade");
}
@NotCallOrigin
public abstract void run(@NonNull Runnable runnable);
private <IN, OUT> IAltFuture<IN, OUT> runAltFuture(@NonNull IRunnableAltFuture<IN, OUT> altFuture) {
run(altFuture);
return altFuture;
}
@Override
@NonNull
@NotCallOrigin
public <IN> Runnable wrapActionWithErrorProtection(@NonNull IAction<IN> action) {
return new Runnable() {
@Override
@NotCallOrigin
public void run() {
try {
action.call();
} catch (Exception e) {
RCLog.e(this, "run(IAction) problem", e);
}
}
};
}
@Override
@NonNull
@NotCallOrigin
public <IN> Runnable wrapActionWithErrorProtection(@NonNull IAction<IN> action,
@NonNull IActionOne<Exception> onErrorAction) {
return new Runnable() {
@Override
@NotCallOrigin
public void run() {
try {
action.call();
} catch (Exception e) {
RCLog.e(this, "run(Runnable) problem", e);
try {
onErrorAction.call(e);
} catch (Exception e1) {
RCLog.e(this, "run(Runnable) problem " + e + " lead to another problem in onErrorAction", e1);
}
}
}
};
}
//========================== .subscribe() and .run() Methods ================================
@Override // IThreadType
@NotCallOrigin
public <IN> void execute(@NonNull IAction<IN> action) {
run(wrapActionWithErrorProtection(action));
}
@Override // IThreadType
@NotCallOrigin
public <IN> void run(@NonNull IAction<IN> action,
@NonNull IActionOne<Exception> onErrorAction) {
run(wrapActionWithErrorProtection(action, onErrorAction));
}
@Override // IThreadType
@NotCallOrigin
public <IN> void runNext(@NonNull IAction<IN> action) {
runNext(wrapActionWithErrorProtection(action));
}
@Override // IThreadType
@SuppressWarnings("unchecked")
public boolean moveToHeadOfQueue(@NonNull Runnable runnable) {
//TODO Analyze if this non-atomic operation is a risk for closing a ThreadType and moving all pending actions to a new thread type as we would like to do for NET_READ when the available bandwdith changes
if (queue instanceof Deque) {
final boolean moved = queue.remove(runnable);
if (moved) {
((Deque<Runnable>) queue).addFirst(runnable);
}
RCLog.v(this, "moveToHeadOfQueue() moved=" + moved);
return moved;
}
return false; // The UI thread does not have a visible queue, and some queues choose not to support re-ordering
}
@Override // IThreadType
@NotCallOrigin
public <IN> void runNext(@NonNull IAction<IN> action,
@NonNull IActionOne<Exception> onErrorAction) {
RCLog.v(this, "runNext()");
runNext(wrapActionWithErrorProtection(action, onErrorAction));
}
@Override // IThreadType
@NonNull
@CheckResult(suggest = IAltFuture.CHECK_RESULT_SUGGESTION)
public <IN> IAltFuture<IN, IN> then(@NonNull IAction<IN> action) {
return runAltFuture(new RunnableAltFuture<>(this, action));
}
@Override // IThreadType
@NonNull
@CheckResult(suggest = IAltFuture.CHECK_RESULT_SUGGESTION)
public <IN> IAltFuture<IN, IN> then(@NonNull IActionOne<IN> action) {
return runAltFuture(new RunnableAltFuture<IN, IN>(this, action));
}
@Override // IThreadType
@NonNull
@CheckResult(suggest = IAltFuture.CHECK_RESULT_SUGGESTION)
public <IN, OUT> IAltFuture<IN, OUT> map(@NonNull IActionOneR<IN, OUT> action) {
return runAltFuture(new RunnableAltFuture<>(this, action));
}
@Override // IThreadType
@NonNull
@CheckResult(suggest = IAltFuture.CHECK_RESULT_SUGGESTION)
public <IN, OUT> IAltFuture<IN, OUT> then(@NonNull IActionR<OUT> action) {
return runAltFuture(new RunnableAltFuture<>(this, action));
}
@Override // IThreadType
@NonNull
@CheckResult(suggest = IAltFuture.CHECK_RESULT_SUGGESTION)
public <OUT> ISettableAltFuture<OUT> from(@NonNull OUT value) {
return new SettableAltFuture<>(this, value);
}
@Override // IThreadType
@NonNull
@CheckResult(suggest = IAltFuture.CHECK_RESULT_SUGGESTION)
public <OUT> ISettableAltFuture<OUT> from() {
return new SettableAltFuture<>(this);
}
@Override // IThreadType
@SafeVarargs
@NonNull
@CheckResult(suggest = IAltFuture.CHECK_RESULT_SUGGESTION)
public final <IN> List<IAltFuture<IN, IN>> then(@NonNull IAction<IN>... actions) {
List<IAltFuture<IN, IN>> altFutures = new ArrayList<>(actions.length);
RCLog.v(this, "map(List[" + actions.length + "])");
for (final IAction<IN> action : actions) {
altFutures.add(then(action));
}
return altFutures;
}
@Override // IThreadType
@SafeVarargs
@NonNull
@CheckResult(suggest = IAltFuture.CHECK_RESULT_SUGGESTION)
public final <IN, OUT> List<IAltFuture<IN, OUT>> then(@NonNull IActionR<OUT>... actions) {
List<IAltFuture<IN, OUT>> altFutures = new ArrayList<>(actions.length);
RCLog.v(this, "map(List[" + actions.length + "])");
for (final IActionR<OUT> action : actions) {
altFutures.add(then(action));
}
return altFutures;
}
@Override // IThreadType
@SafeVarargs
@NonNull
@CheckResult(suggest = IAltFuture.CHECK_RESULT_SUGGESTION)
public final <IN, OUT> List<IAltFuture<IN, OUT>> map(@NonNull IActionOneR<IN, OUT>... actions) {
final List<IAltFuture<IN, OUT>> altFutures = new ArrayList<>(actions.length);
for (final IActionOneR<IN, OUT> action : actions) {
altFutures.add(map(action));
}
return altFutures;
}
//TODO add mapEach(IActionOneR) from list to list
//TODO add thenEach(IActionOne) from list
//=============================== Public Utility Methods ======================================
//TODO public <A> RunnableAltFuture<A> flush() - current thread type - wait for everything forked before this point and their side effects queued before other things to complete before next step on the specified threadtype
@Override // IThreadType
public <IN, OUT> void fork(@NonNull IRunnableAltFuture<IN, OUT> runnableAltFuture) {
AssertUtil.assertTrue("Call runnableAltFuture().fork() instead. AbstractThreadType.fork() expected the IRunnableAltFuture should return isForked() and !isDone()", runnableAltFuture.isForked());
if (BuildConfig.DEBUG) {
if (isMistakenlyCalledDirectlyFromOutsideTheCascadeLibrary()) {
throw new UnsupportedOperationException("Method for internal use only. Call " + runnableAltFuture + ".fork() instead. If you are implementing your own IAltFuture, do so in " + Async.class.getPackage());
}
if (runnableAltFuture.isDone()) {
RCLog.v(this, "Warning: fork() called multiple times");
}
}
run(runnableAltFuture); // Atomic state checks must be completed later in the .run() method
}
@Override // IThreadType
public boolean isShutdown() {
return executorService.isShutdown();
}
@Override // IThreadType
@NonNull
public <IN> Future<Boolean> shutdown(long timeout,
@Nullable IAction<IN> afterShutdownAction) {
if (timeout < 1) {
RCLog.throwIllegalArgumentException(this, "shutdown(" + timeout + ") is illegal, time must be > 0");
}
if (timeout == 0 && afterShutdownAction != null) {
RCLog.throwIllegalArgumentException(this, "shutdown(0) is legal, but do not supply a afterShutdownAction() as it would run immediately which is probably an error");
}
final ImmutableValue<String> origin = RCLog.originAsync()
.then(o -> {
RCLog.i(this, "shutdown " + timeout + " mOrigin=" + o + " ThreadType");
executorService.shutdown();
});
final FutureTask<Boolean> futureTask = new FutureTask<>(() -> {
boolean terminated = timeout == 0;
try {
if (!terminated) {
terminated = executorService.awaitTermination(timeout, TimeUnit.MILLISECONDS);
}
} catch (InterruptedException e) {
Log.e(AbstractThreadType.class.getSimpleName(), "Could not shutdown. afterShutdownAction will not be called: " + origin, e);
terminated = false;
} catch (Exception e) {
RCLog.e(this, "Could not shutdown. afterShutdownAction will not be called: " + origin, e);
terminated = false;
} finally {
if (terminated && afterShutdownAction != null) {
try {
afterShutdownAction.call();
} catch (Exception e) {
RCLog.e(this, "Problem during afterShutdownAction after successful workerExecutorService.shutdown: " + origin, e);
terminated = false;
}
}
}
return terminated;
});
(new Thread(futureTask, "Shutdown ThreadType " + getName())).start();
return futureTask;
}
@Override // IThreadType
@NonNull
public <IN> List<Runnable> shutdownNow(@NonNull String reason,
@Nullable IAction<IN> actionOnDedicatedThreadAfterAlreadyStartedTasksComplete,
@Nullable IAction<IN> actionOnDedicatedThreadIfTimeout,
long timeoutMillis) {
RCLog.i(this, "shutdownNow: reason=" + reason);
List<Runnable> pendingActions = executorService.shutdownNow();
if (actionOnDedicatedThreadAfterAlreadyStartedTasksComplete != null) {
new Thread(() -> {
try {
if (executorService.awaitTermination(timeoutMillis, TimeUnit.MILLISECONDS)) {
actionOnDedicatedThreadAfterAlreadyStartedTasksComplete.call();
} else {
if (actionOnDedicatedThreadIfTimeout != null) {
actionOnDedicatedThreadIfTimeout.call();
} else {
Log.i(AbstractThreadType.class.getSimpleName(), "Timeout in shutdownNow, reason=" + reason + " for ThreadType " + getName() + ". Consider providing a non-null actionOnDedicatedThreadIfTimeout");
}
}
} catch (Exception e) {
Log.e(AbstractThreadType.class.getSimpleName(), "Problem in awaitTermination for ServiceExecutor, reason=" + reason + ", ThreadType " + getName(), e);
}
}, "shutdownNow" + reason)
.start();
}
return pendingActions;
}
@Override // INamed
@NonNull
public String getName() {
return name;
}
@NonNull
@Override // Object
public String toString() {
return getName();
}
}