All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.diversionmc.async.schedule.ThreadPoolScheduler Maven / Gradle / Ivy

There is a newer version: 2.0.0
Show newest version
package net.diversionmc.async.schedule;

import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.ForkJoinWorkerThread;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import static java.lang.Runtime.getRuntime;
import static java.lang.Thread.currentThread;
import static java.lang.Thread.sleep;
import static java.text.MessageFormat.format;
import static java.util.concurrent.TimeUnit.SECONDS;
import static java.util.stream.IntStream.range;

/**
 * A scheduler that performs action in its own sustained threads.
 */
public final class ThreadPoolScheduler implements Scheduler {
    /**
     * Instance of this thread pool scheduler.
     */
    public static final ThreadPoolScheduler SCHEDULER = new ThreadPoolScheduler();
    public static final long AWAIT_TIMEOUT_MILLIS = 5000;

    private final LinkedBlockingDeque tasks = new LinkedBlockingDeque<>();
    private final List threads = new ArrayList<>();
    private final Lock runLock = new ReentrantLock();
    private final Object runLockUnlock = new Object();

    private ThreadPoolScheduler() {
        // Create thread that owns the runLock and unlocks it when runLockUnlock is notified
        var unlockThread = new Thread(() -> {
            runLock.lock();
            try {
                synchronized (runLockUnlock) {
                    runLockUnlock.wait();
                }
            } catch (InterruptedException ignored) {
            } finally {
                // The unlocked runLock will cause all repeat and delay promises to stop waiting
                runLock.unlock();
            }
        });
        unlockThread.setDaemon(true);
        unlockThread.start();

        // Create a shutdown hook that calls notify on runLockUnlock and waits for threads to finish
        getRuntime().addShutdownHook(new Thread(() -> {
            try (var out = new PrintStream(new FileOutputStream(FileDescriptor.out))) {
                out.println("Shutting down promise worker thread pool...");
                synchronized (runLockUnlock) {
                    runLockUnlock.notify();
                }
                long timePassed = 0;
                while (true) {
                    ArrayList threadsCopy;
                    synchronized (threads) {
                        if (threads.isEmpty()) break;
                        threadsCopy = new ArrayList<>(threads);
                    }
                    out.println(format(
                        "Current status: {0} worker threads, {1}s passed",
                        threadsCopy.size(), timePassed));
                    if (threadsCopy.stream()
                        .noneMatch(Thread::isAlive)) break;
                    threadsCopy.stream()
                        .filter(Thread::isAlive)
                        .forEach(thread -> {
                            out.println("Worker thread " + thread.getName() + " is still alive:");
                            Arrays.stream(thread.getStackTrace())
                                .forEach(traceElement -> out.println("\tat " + traceElement));
                        });
                    try {
                        //noinspection BusyWait
                        sleep(AWAIT_TIMEOUT_MILLIS);
                        timePassed += AWAIT_TIMEOUT_MILLIS / 1000;
                    } catch (InterruptedException ignored) {
                    }
                }
                out.println("Promise worker thread pool has finished working.");
            }
        }));
    }

    public Lock runLock() {
        return runLock;
    }

    private void ensureEnoughThreads() {
        // 10 tasks - 10 threads = 0
        // 10 tasks -  9 threads = 1 new thread
        // at worst, it will create useless threads - never not enough threads
        synchronized (threads) {
            range(0, tasks.size() - (int) threads.stream().filter(t -> !t.busy.get()).count())
                .forEach(i -> {
                    var t = new DNThread();
                    t.setDaemon(true);
                    t.start();
                    threads.add(t);
                });
        }
    }

    private class DNThread extends Thread {
        private final AtomicBoolean busy = new AtomicBoolean();

        public void run() {
            while (!tasks.isEmpty()) try {
                var task = tasks.poll(5, SECONDS);
                // here can be a period, when there is a non-busy thread, but task is removed
                if (task == null) break; // die if thread inactive for 5 seconds
                busy.set(true);
                try {
                    task.runnable.run();
                } catch (Throwable e) { // do not break worker thread
                    e.printStackTrace();
                }
                task.quietlyComplete();
                busy.set(false);
            } catch (InterruptedException ignored) {
                // unwanted exception, retry waiting
            }
            synchronized (threads) {
                threads.remove(this);
            }
        }
    }

    /**
     * Check if this method is called from a thread pool thread or fork join worker thread.
     *
     * @return True if current thread is a generated thread.
     */
    public static boolean allowedAwait() {
        var t = currentThread();
        return t instanceof DNThread || t instanceof ForkJoinWorkerThread;
    }

    /**
     * Adds a task to the thread pool.
     *
     * @param asyncAction Action to perform.
     * @return Created task future.
     */
    public Future schedule(Runnable asyncAction) {
        var task = new DummyTask(asyncAction);
        tasks.add(task);
        ensureEnoughThreads();
        return task;
    }

    /**
     * Task that always counts as completed.
     */
    public static final class DummyTask extends ForkJoinTask {
        private Runnable runnable;

        public DummyTask() {
        }

        private DummyTask(Runnable r) {
            runnable = r;
        }

        @SuppressWarnings("ReturnOfNull")
        public Object getRawResult() {
            return null;
        }

        protected void setRawResult(Object value) {
        }

        protected boolean exec() {
            return true;
        }
    }
}