package the8472.utils.concurrent;
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.Collection;
import java.util.List;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.RunnableScheduledFuture;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;
/**
* used a linked transfer queue for non-scheduled tasks to achieve high throughput / avoid lock contention.
*
* scheduled tasks are handled by a dedicated runnable executed in one of the executor's threads
* - gets re-circulated through the LTQ
* - yields its own thread back to the executor when load depands it
* - only ever that one instance active at a time -> sequential behavior -> we can use a lock-free priority queue
* - wakeups happen either based on the next scheduled task or cooperatively with executor methods
*
*/
public class NonblockingScheduledExecutor implements ScheduledExecutorService {
WrappedThreadPoolExecutor immediateExecutor;
ThreadGroup group;
PriorityQueue<RunnableScheduledFuture<?>> delayedTasks = new PriorityQueue<>();
AtomicReference<Thread> currentSleeper = new AtomicReference<>();
Queue<RunnableScheduledFuture<?>> submittedScheduledTasks = new ConcurrentLinkedQueue<>();
BlockingQueue<Runnable> executorQueue = new LinkedTransferQueue<>();
// there is only ever one single instance of this task in the pipeline. therefore anything done by it is guaranteed single-threaded execution
final Runnable scheduler = this::doStateMaintenance;
final Thread.UncaughtExceptionHandler exceptionHandler;
private class WrappedThreadPoolExecutor extends ThreadPoolExecutor {
public WrappedThreadPoolExecutor(int poolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(poolSize, poolSize, keepAliveTime, unit, workQueue);
}
@Override
public void execute(Runnable command) {
super.execute(command);
wakeupWaiter(false);
}
void executeWithoutWakeup(Runnable command) {
super.execute(command);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
if(exceptionHandler != null && r instanceof FutureTask<?>) {
FutureTask<?> ft = (FutureTask<?>) r;
if(ft.isDone() && !ft.isCancelled()) {
try {
ft.get();
} catch (InterruptedException | ExecutionException e) {
exceptionHandler.uncaughtException(null, e.getCause());
}
}
}
}
}
public NonblockingScheduledExecutor(String name, int threadCount, UncaughtExceptionHandler handler) {
this.exceptionHandler = handler;
group = new ThreadGroup(name);
group.setDaemon(true);
ThreadFactory f = (r) -> {
Thread t = new Thread(group, r);
if(handler != null)
t.setUncaughtExceptionHandler(handler);
t.setDaemon(true);
t.setName(name);
return t;
};
immediateExecutor = new WrappedThreadPoolExecutor(threadCount, 4, TimeUnit.SECONDS, executorQueue);
immediateExecutor.setThreadFactory(f);
immediateExecutor.execute(scheduler);
}
void doStateMaintenance() {
while(!isShutdown()) {
RunnableScheduledFuture<?> toSchedule;
while((toSchedule = submittedScheduledTasks.poll()) != null)
delayedTasks.add(toSchedule);
RunnableScheduledFuture<?> toExecute;
while((toExecute = delayedTasks.peek()) != null && toExecute.getDelay(TimeUnit.NANOSECONDS) <= 0) {
delayedTasks.poll();
immediateExecutor.executeWithoutWakeup(toExecute);
}
RunnableScheduledFuture<?> nextTask = delayedTasks.peek();
// signal current thread as suspended before we actually check work queues.
// this avoids wakeupWaiter() seeing an inconsistent state
currentSleeper.set(Thread.currentThread());
if(executorQueue.isEmpty() && submittedScheduledTasks.isEmpty()) {
if(nextTask != null)
LockSupport.parkNanos(nextTask.getDelay(TimeUnit.NANOSECONDS));
else
LockSupport.park();
currentSleeper.set(null);
} else {
currentSleeper.set(null);
// there are unmatched tasks in the queue, return this thread to the pool
break;
}
}
// reschedule if we fall out of loop
if(!isShutdown())
immediateExecutor.executeWithoutWakeup(scheduler);
}
void wakeupWaiter(boolean forScheduled) {
Thread t;
while((t = currentSleeper.get()) != null && (forScheduled ? !submittedScheduledTasks.isEmpty() : !executorQueue.isEmpty())) {
if(currentSleeper.compareAndSet(t, null)) {
LockSupport.unpark(t);
break;
}
}
}
@Override
public void shutdown() {
immediateExecutor.shutdown();
wakeupWaiter(true);
}
@Override
public List<Runnable> shutdownNow() {
List<Runnable> l = immediateExecutor.shutdownNow();
wakeupWaiter(true);
return l;
}
@Override
public boolean isShutdown() {
return immediateExecutor.isShutdown();
}
@Override
public boolean isTerminated() {
return immediateExecutor.isTerminated();
}
@Override
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
return immediateExecutor.awaitTermination(timeout, unit);
}
@Override
public <T> Future<T> submit(Callable<T> task) {
return immediateExecutor.submit(task);
}
@Override
public <T> Future<T> submit(Runnable task, T result) {
return immediateExecutor.submit(task, result);
}
@Override
public Future<?> submit(Runnable task) {
return immediateExecutor.submit(task);
}
@Override
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException {
return immediateExecutor.invokeAll(tasks);
}
@Override
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException {
return immediateExecutor.invokeAll(tasks, timeout, unit);
}
@Override
public <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException {
return immediateExecutor.invokeAny(tasks);
}
@Override
public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
return immediateExecutor.invokeAny(tasks, timeout, unit);
}
@Override
public void execute(Runnable command) {
immediateExecutor.execute(command);
}
private class SchedF<T> extends FutureTask<T> implements RunnableScheduledFuture<T> {
long nanos;
final long period;
public SchedF(Runnable r, long delay, TimeUnit u) {
super(r,null);
this.nanos = System.nanoTime() + TimeUnit.NANOSECONDS.convert(delay, u);
this.period = 0;
}
public SchedF(Callable<T> callable, long delay, TimeUnit u) {
super(callable);
this.nanos = System.nanoTime() + TimeUnit.NANOSECONDS.convert(delay, u);
period = 0;
}
public SchedF(Runnable command, long initialDelay, long period, TimeUnit unit) {
super(command, null);
this.nanos = System.nanoTime() + TimeUnit.NANOSECONDS.convert(initialDelay, unit);
this.period = TimeUnit.NANOSECONDS.convert(period, unit);
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(nanos - System.nanoTime(), TimeUnit.NANOSECONDS);
}
@Override
public int compareTo(Delayed o) {
long diff;
if(o instanceof SchedF) {
SchedF<?> otherS = (SchedF<?>) o;
diff = this.nanos - otherS.nanos;
} else {
diff = getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS);
}
return (diff == 0) ? 0 : (diff < 0) ? -1 : 1;
}
@Override
public boolean isPeriodic() {
return period != 0;
}
void recalcTime() {
if(period < 0) {
nanos = System.nanoTime() + (-period);
} else {
nanos += period;
}
}
@Override
public void run() {
if(isPeriodic()) {
if(runAndReset()) {
recalcTime();
submittedScheduledTasks.add(this);
wakeupWaiter(true);
}
} else {
super.run();
}
}
}
@Override
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
// roll our own to avoid thread suspension due to queue locking when hammering the scheduler with lots of one-off submissions
SchedF<?> future = new SchedF<Void>(command, delay, unit);
submittedScheduledTasks.add(future);
wakeupWaiter(true);
return future;
}
@Override
public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) {
SchedF<V> future = new SchedF<>(callable, delay, unit);
submittedScheduledTasks.add(future);
wakeupWaiter(true);
return future;
}
@Override
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {
if(period < 0)
throw new IllegalArgumentException("delay must be non-negative");
SchedF<?> future = new SchedF<Void>(command, initialDelay, period, unit);
submittedScheduledTasks.add(future);
wakeupWaiter(true);
return future;
}
@Override
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) {
if(delay < 0)
throw new IllegalArgumentException("delay must be non-negative");
SchedF<?> future = new SchedF<Void>(command, initialDelay, -delay, unit);
submittedScheduledTasks.add(future);
wakeupWaiter(true);
return future;
}
}