package org.threadly.concurrent.wrapper.compatibility;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.threadly.concurrent.RunnableContainer;
import org.threadly.concurrent.SchedulerService;
import org.threadly.concurrent.future.FutureUtils;
import org.threadly.concurrent.future.ListenableFuture;
import org.threadly.concurrent.future.ListenableFutureTask;
import org.threadly.concurrent.future.ListenableScheduledFuture;
import org.threadly.util.ArgumentVerifier;
import org.threadly.util.Clock;
import org.threadly.util.ExceptionUtils;
/**
* Generic implementation for the wrappers that implement {@link ScheduledExecutorService}. This
* allows us to add new wrappers with the minimal amount of duplicated code.
*
* @since 4.6.0 (since 2.0.0 at org.threadly.concurrent)
*/
abstract class AbstractExecutorServiceWrapper implements ScheduledExecutorService {
protected final SchedulerService scheduler;
/**
* Constructs a new wrapper to adhere to the {@link ScheduledExecutorService} interface.
*
* @param scheduler scheduler implementation to rely on
*/
public AbstractExecutorServiceWrapper(SchedulerService scheduler) {
ArgumentVerifier.assertNotNull(scheduler, "scheduler");
this.scheduler = scheduler;
}
@Override
public boolean isShutdown() {
return scheduler.isShutdown();
}
@Override
public <T> ListenableFuture<T> submit(Callable<T> task) {
return scheduler.submit(task);
}
@Override
public <T> ListenableFuture<T> submit(Runnable task, T result) {
return scheduler.submit(task, result);
}
@Override
public ListenableFuture<?> submit(Runnable task) {
return scheduler.submit(task);
}
@Override
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException {
return invokeAll(tasks, Long.MAX_VALUE, TimeUnit.MILLISECONDS);
}
@Override
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit) throws InterruptedException {
long timeoutInMs = unit.toMillis(timeout);
long startTime = timeoutInMs < Long.MAX_VALUE ? Clock.accurateForwardProgressingMillis() : -1;
List<Future<T>> resultList = new ArrayList<>(tasks.size());
// execute all the tasks provided
{
Iterator<? extends Callable<T>> it = tasks.iterator();
while (it.hasNext()) {
Callable<T> c = it.next();
if (c == null) {
throw new NullPointerException();
}
ListenableFuture<T> lf = scheduler.submit(c);
resultList.add(lf);
}
}
// block till all tasks finish, or we reach our timeout
if (timeoutInMs < Long.MAX_VALUE) {
long remainingTime = timeoutInMs - (Clock.accurateForwardProgressingMillis() - startTime);
try {
FutureUtils.blockTillAllComplete(resultList, remainingTime);
} catch (TimeoutException e) {
FutureUtils.cancelIncompleteFutures(resultList, true);
}
} else {
FutureUtils.blockTillAllComplete(resultList);
}
return resultList;
}
@Override
public <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException,
ExecutionException {
try {
return invokeAny(tasks, Long.MAX_VALUE, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
// basically impossible
throw ExceptionUtils.makeRuntime(e);
}
}
@Override
public <T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit) throws InterruptedException,
ExecutionException,
TimeoutException {
if (tasks.size() < 1) {
throw new IllegalArgumentException("Empty task list provided");
}
final long startTime = Clock.accurateForwardProgressingMillis();
final long timeoutInMs = unit.toMillis(timeout);
int failureCount = 0;
// going to be optimistic and allocate the initialize size so that at most we have to do one expansion
List<Future<T>> submittedFutures = new ArrayList<>((tasks.size() / 2) + 1);
try {
ExecutorCompletionService<T> ecs = new ExecutorCompletionService<>(this);
ExecutionException lastEE = null;
Iterator<? extends Callable<T>> it = tasks.iterator();
// submit first one
submittedFutures.add(ecs.submit(it.next()));
long remainingTime = timeoutInMs - (Clock.lastKnownForwardProgressingMillis() - startTime);
while (it.hasNext() && remainingTime > 0) {
Future<T> completedFuture = ecs.poll();
if (completedFuture == null) {
// submit another
submittedFutures.add(ecs.submit(it.next()));
} else {
try {
return completedFuture.get();
} catch (ExecutionException e) {
failureCount++;
lastEE = e;
}
}
remainingTime = timeoutInMs - (Clock.accurateForwardProgressingMillis() - startTime);
}
// we must compare against failure count otherwise we may throw a TimeoutException when all tasks have failed
while (remainingTime > 0 && failureCount < submittedFutures.size()) {
Future<T> completedFuture = ecs.poll(remainingTime, TimeUnit.MILLISECONDS);
if (completedFuture == null) {
throw new TimeoutException();
} else {
try {
return completedFuture.get();
} catch (ExecutionException e) {
failureCount++;
lastEE = e;
}
}
remainingTime = timeoutInMs - (Clock.accurateForwardProgressingMillis() - startTime);
}
if (remainingTime <= 0) {
throw new TimeoutException();
} else {
/* since we know we have at least one task provided, and since nothing returned by this point
* we know that we only got ExecutionExceptions, and thus this should NOT be null
*/
throw lastEE;
}
} finally {
FutureUtils.cancelIncompleteFutures(submittedFutures, true);
}
}
@Override
public void execute(Runnable task) {
scheduler.execute(task);
}
@Override
public ListenableScheduledFuture<?> schedule(Runnable task, long delay, TimeUnit unit) {
if (task == null) {
throw new NullPointerException("Must provide task");
} else if (delay < 0) {
delay = 0;
}
return schedule(task, unit.toMillis(delay));
}
protected abstract ListenableScheduledFuture<?> schedule(Runnable task, long delayInMillis);
@Override
public <V> ListenableScheduledFuture<V> schedule(Callable<V> callable, long delay,
TimeUnit unit) {
if (callable == null) {
throw new NullPointerException("Must provide task");
} else if (delay < 0) {
delay = 0;
}
return schedule(callable, unit.toMillis(delay));
}
protected abstract <V> ListenableScheduledFuture<V> schedule(Callable<V> callable, long delayInMillis);
@Override
public ListenableScheduledFuture<?> scheduleWithFixedDelay(Runnable task,
long initialDelay,
long delay, TimeUnit unit) {
if (task == null) {
throw new NullPointerException("Must provide task");
} else if (delay <= 0) {
throw new IllegalArgumentException();
} else if (initialDelay < 0) {
initialDelay = 0;
}
long initialDelayInMs = unit.toMillis(initialDelay);
long delayInMs = unit.toMillis(delay);
return scheduleWithFixedDelay(task, initialDelayInMs, delayInMs);
}
protected abstract ListenableScheduledFuture<?> scheduleWithFixedDelay(Runnable task,
long initialDelayInMillis,
long delayInMillis);
@Override
public ListenableScheduledFuture<?> scheduleAtFixedRate(Runnable task,
long initialDelay, long period,
TimeUnit unit) {
if (task == null) {
throw new NullPointerException("Must provide task");
} else if (period <= 0) {
throw new IllegalArgumentException();
} else if (initialDelay < 0) {
initialDelay = 0;
}
long initialDelayInMs = unit.toMillis(initialDelay);
long periodInMs = unit.toMillis(period);
return scheduleAtFixedRate(task, initialDelayInMs, periodInMs);
}
protected abstract ListenableScheduledFuture<?> scheduleAtFixedRate(Runnable task,
long initialDelayInMillis,
long periodInMillis);
/**
* Because in {@link java.util.concurrent.ScheduledExecutorService} an exception from a
* recurring task causes the task to stop executing, we have to wrap the task. That way we can
* remove the recurring task if the error occurs (since
* {@link org.threadly.concurrent.SubmitterScheduler} will continue to execute the task despite
* the error.
*
* @since 2.1.0
*/
protected static class ThrowableHandlingRecurringRunnable implements RunnableContainer, Runnable {
private final SchedulerService scheduler;
private final Runnable task;
protected ThrowableHandlingRecurringRunnable(SchedulerService scheduler, Runnable task) {
this.scheduler = scheduler;
this.task = task;
}
@Override
public void run() {
try {
task.run();
} catch (Throwable t) {
scheduler.remove(this);
ExceptionUtils.handleException(t);
}
}
@Override
public Runnable getContainedRunnable() {
return task;
}
}
/**
* An implementation of {@link ListenableFutureTask} which will remove the task from the
* scheduler when cancel is invoked. Threadly does not normally have this behavior for a
* couple reasons. Because we don't return futures on recurring tasks, canceling a future just
* results in a one time task execution that is a quick no-op. It is cheaper in threadly to
* allow this no-op task on .cancel than to attempt removal. Because
* {@link ScheduledExecutorService} returns a future that can be canceled for recurring tasks,
* we want to go ahead and remove the task (rather than have recurring no-op executions).
*
* @since 4.4.3
* @param <T> The result object type returned by this future
*/
protected static class CancelRemovingListenableFutureTask<T> extends ListenableFutureTask<T> {
private final SchedulerService scheduler;
public CancelRemovingListenableFutureTask(SchedulerService scheduler,
boolean recurring, Runnable task) {
super(recurring, task);
this.scheduler = scheduler;
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
if (super.cancel(mayInterruptIfRunning)) {
scheduler.remove(this.getContainedCallable());
return true;
} else {
return false;
}
}
}
}