package tc.oc.commons.core.scheduler; import java.util.Collections; import java.util.HashSet; import java.util.Set; import java.util.logging.Logger; import java.util.stream.Collectors; import javax.annotation.Nullable; import javax.inject.Inject; import com.google.common.collect.ImmutableSet; import java.time.Duration; import java.time.Instant; import tc.oc.commons.core.concurrent.CatchingRunnable; import tc.oc.commons.core.exception.ExceptionHandler; import tc.oc.commons.core.logging.Loggers; import tc.oc.commons.core.plugin.PluginScoped; import tc.oc.commons.core.util.StackTrace; /** * Represents a disposable {@link Task} scheduler. * * This mostly acts as a wrapper for the Bukkit/Bungee schedulers, however * it adds additional functionality such as debouncing, phased registration, * and universal cancellation. */ @PluginScoped public class Scheduler { protected final Logger logger; private final SchedulerBackend<Object> backend; private final ExceptionHandler exceptionHandler; private final Set<AbstractTask> registered = new HashSet<>(); private final Set<AbstractTask> started = new HashSet<>(); private final Set<Class<?>> skipTraceClasses = ImmutableSet.of(getClass(), State.class, Unstarted.class, Cancelled.class, Running.class); private State state; @Inject protected Scheduler(Loggers loggers, SchedulerBackend backend, ExceptionHandler exceptionHandler) { this(loggers, backend, exceptionHandler, true); } public Scheduler(Loggers loggers, SchedulerBackend backend, ExceptionHandler exceptionHandler, boolean started) { this.exceptionHandler = exceptionHandler; this.logger = loggers.get(getClass()); this.backend = backend; state = started ? new Running() : new Unstarted(); } protected Task register(Task.Parameters parameters, Runnable runnable) { return register(parameters, runnable, new StackTrace(skipTraceClasses)); } @Override public String toString() { return getClass().getSimpleName() + "{registered=" + registered + ", started=" + started + ", state=" + state.getClass().getSimpleName() + "}"; } public Task createTask(Runnable task) { return register(Task.Parameters.fromDuration(null, null), task); } public Task createDelayedTask(Duration delay, Runnable task) { return register(Task.Parameters.fromDuration(delay, null), task); } public Task createDelayedTask(long delay, Runnable task) { return register(Task.Parameters.fromTicks(delay, null), task); } public Task createDelayedTask(Instant when, Runnable task) { final Instant now = Instant.now(); if(when.isAfter(Instant.now())) { return createDelayedTask(Duration.between(now, when), task); } else { return createTask(task); } } public Task createRepeatingTask(Duration interval, Runnable task) { return register(Task.Parameters.fromDuration(Duration.ZERO, interval), task); } public Task createRepeatingTask(long interval, Runnable task) { return register(Task.Parameters.fromTicks(0L, interval), task); } public Task createRepeatingTask(Duration delay, Duration interval, Runnable task) { return register(Task.Parameters.fromDuration(delay, interval), task); } public Task createRepeatingTask(long delay, long interval, Runnable task) { return register(Task.Parameters.fromTicks(delay, interval), task); } /** * Create a new {@link ReusableTask} with no schedule */ public ReusableTask createReusableTask(Runnable runnable) { return new ReusableTask(this, new CatchingRunnable(exceptionHandler, runnable, new StackTrace(skipTraceClasses))); } public DebouncedTask createDebouncedTask(Duration delay, Runnable runnable) { return new DebouncedTask(this, delay, runnable); } public DebouncedTask createDebouncedTask(Runnable runnable) { return new DebouncedTask(this, runnable); } // Keep all synchronized methods below /** * Start all registered {@link Task}s from now on. */ synchronized public void start() { state.start(); } /** * Permanently disable this scheduler and cancel all {@link Task}s in the past and future. */ synchronized public void cancel() { state.cancel(); } synchronized protected Task register(Task.Parameters parameters, Runnable runnable, @Nullable StackTrace trace) { return state.register(parameters, new CatchingRunnable(exceptionHandler, runnable, trace)); } synchronized protected boolean isTaskQueued(AbstractTask task) { return state.isTaskQueued(task); } synchronized protected boolean isTaskRunning(AbstractTask task) { return state.isTaskRunning(task); } synchronized protected void cancelTask(AbstractTask task) { state.cancelTask(task); } /** * Run the given task only if there is no instance of that task's class already scheduled or running. * @return The handle of the newly scheduled task, if it was scheduled, otherwise the handle of the existing task. */ synchronized public Task debounceTask(Runnable runnable) { for(Task task : state.pendingTasks()) { if(task.isPending() && task.getRunnable().getClass().isInstance(runnable)) { return task; } } return createTask(runnable); } protected Object startTask(AbstractTask task) { final Runnable runnable; if(task.getParameters().isRepeating()) { runnable = task.getRunnable(); } else { runnable = () -> { try { task.getRunnable().run(); } finally { synchronized(Scheduler.this) { started.remove(task); } } }; } final Object backendTask = backend.startTask(task.getParameters(), runnable); task.setRunning(backendTask); synchronized(this) { started.add(task); } return backendTask; } private abstract class State { void start() {}; void cancel() {}; void cancelTask(AbstractTask task) {} abstract Iterable<? extends Task> pendingTasks(); abstract boolean isTaskQueued(AbstractTask task); abstract boolean isTaskRunning(AbstractTask task); DisposableTask register(Task.Parameters parameters, CatchingRunnable runnable) { return new DisposableTask(Scheduler.this, parameters, runnable); } } private class Unstarted extends State { @Override void start() { registered.forEach(Scheduler.this::startTask); registered.clear(); state = new Running(); } @Override void cancel() { registered.clear(); state = new Cancelled(); } @Override DisposableTask register(Task.Parameters parameters, CatchingRunnable runnable) { final DisposableTask task = super.register(parameters, runnable); registered.add(task); return task; } @Override void cancelTask(AbstractTask task) { registered.remove(task); } @Override boolean isTaskQueued(AbstractTask task) { return registered.contains(task); } @Override boolean isTaskRunning(AbstractTask task) { return false; } @Override public Iterable<? extends Task> pendingTasks() { return registered; } } private class Running extends State { @Override void cancel() { ImmutableSet.copyOf(started).forEach(Task::cancel); started.clear(); state = new Cancelled(); } @Override DisposableTask register(Task.Parameters parameters, CatchingRunnable runnable) { final DisposableTask task = super.register(parameters, runnable); startTask(task); return task; } @Override boolean isTaskQueued(AbstractTask task) { return task.backend != null && backend.isTaskQueued(task.backend); } @Override boolean isTaskRunning(AbstractTask task) { return task.backend != null && backend.isTaskRunning(task.backend); } @Override void cancelTask(AbstractTask task) { started.remove(task); if(task.backend != null) { backend.cancelTask(task.backend); } task.setCancelled(); } @Override public Iterable<? extends Task> pendingTasks() { return started .stream() .filter(Task::isPending) .collect(Collectors.toSet()); } } private class Cancelled extends State { @Override void start() { throw new IllegalStateException("Scheduler has already been cancelled"); } @Override DisposableTask register(Task.Parameters parameters, CatchingRunnable runnable) { final DisposableTask task = super.register(parameters, runnable); task.setCancelled(); return task; } @Override boolean isTaskQueued(AbstractTask task) { return false; } @Override boolean isTaskRunning(AbstractTask task) { return false; } @Override public Iterable<Task> pendingTasks() { return Collections.emptySet(); } } }