From ead567fd94bc89cf98e3f633a3224f7d0384fd5a Mon Sep 17 00:00:00 2001 From: Molzonas Date: Sat, 30 Aug 2025 20:39:53 +0200 Subject: [PATCH] Universal Task Scheduling --- .../mzcore/exception/MZTaskException.java | 13 + .../molzonas/mzcore/tasks/MZTaskHandle.java | 13 + .../fr/molzonas/mzcore/tasks/MZTaskInfo.java | 25 ++ .../mzcore/tasks/MZTaskScheduler.java | 316 ++++++++++++++++++ .../tasks/MZTaskSchedulerInterface.java | 41 +++ .../mzcore/tasks/enums/MZTaskKind.java | 5 + .../mzcore/tasks/enums/MZTaskStatus.java | 5 + .../mzcore/tasks/enums/MZTaskType.java | 5 + 8 files changed, 423 insertions(+) create mode 100644 src/main/java/fr/molzonas/mzcore/exception/MZTaskException.java create mode 100644 src/main/java/fr/molzonas/mzcore/tasks/MZTaskHandle.java create mode 100644 src/main/java/fr/molzonas/mzcore/tasks/MZTaskInfo.java create mode 100644 src/main/java/fr/molzonas/mzcore/tasks/MZTaskScheduler.java create mode 100644 src/main/java/fr/molzonas/mzcore/tasks/MZTaskSchedulerInterface.java create mode 100644 src/main/java/fr/molzonas/mzcore/tasks/enums/MZTaskKind.java create mode 100644 src/main/java/fr/molzonas/mzcore/tasks/enums/MZTaskStatus.java create mode 100644 src/main/java/fr/molzonas/mzcore/tasks/enums/MZTaskType.java diff --git a/src/main/java/fr/molzonas/mzcore/exception/MZTaskException.java b/src/main/java/fr/molzonas/mzcore/exception/MZTaskException.java new file mode 100644 index 0000000..7abe70c --- /dev/null +++ b/src/main/java/fr/molzonas/mzcore/exception/MZTaskException.java @@ -0,0 +1,13 @@ +package fr.molzonas.mzcore.exception; + +public class MZTaskException extends RuntimeException { + public MZTaskException(String message) { + super(message); + } + public MZTaskException(Exception e) { + super(e); + } + public MZTaskException(Throwable t) { + super(t); + } +} diff --git a/src/main/java/fr/molzonas/mzcore/tasks/MZTaskHandle.java b/src/main/java/fr/molzonas/mzcore/tasks/MZTaskHandle.java new file mode 100644 index 0000000..b595b0b --- /dev/null +++ b/src/main/java/fr/molzonas/mzcore/tasks/MZTaskHandle.java @@ -0,0 +1,13 @@ +package fr.molzonas.mzcore.tasks; + +import java.time.Duration; +import java.util.UUID; + +public interface MZTaskHandle { + UUID getId(); + boolean cancel(); + boolean isDone(); + boolean isCancelled(); + Duration getDuration(); + Throwable getCause(); +} diff --git a/src/main/java/fr/molzonas/mzcore/tasks/MZTaskInfo.java b/src/main/java/fr/molzonas/mzcore/tasks/MZTaskInfo.java new file mode 100644 index 0000000..2530290 --- /dev/null +++ b/src/main/java/fr/molzonas/mzcore/tasks/MZTaskInfo.java @@ -0,0 +1,25 @@ +package fr.molzonas.mzcore.tasks; + +import fr.molzonas.mzcore.tasks.enums.MZTaskKind; +import fr.molzonas.mzcore.tasks.enums.MZTaskStatus; +import fr.molzonas.mzcore.tasks.enums.MZTaskType; + +import java.time.Duration; +import java.util.UUID; + +public record MZTaskInfo( + UUID id, + MZTaskType type, + MZTaskKind kind, + MZTaskStatus status, + Long start, + Long end, + int runCount, + String note, + Throwable error +) { + public Duration duration() { + long end = (end() != null) ? end() : System.nanoTime(); + return Duration.ofNanos(end - start); + } +} diff --git a/src/main/java/fr/molzonas/mzcore/tasks/MZTaskScheduler.java b/src/main/java/fr/molzonas/mzcore/tasks/MZTaskScheduler.java new file mode 100644 index 0000000..28259fc --- /dev/null +++ b/src/main/java/fr/molzonas/mzcore/tasks/MZTaskScheduler.java @@ -0,0 +1,316 @@ +package fr.molzonas.mzcore.tasks; + +import fr.molzonas.mzcore.exception.MZTaskException; +import fr.molzonas.mzcore.tasks.enums.MZTaskKind; +import fr.molzonas.mzcore.tasks.enums.MZTaskStatus; +import fr.molzonas.mzcore.tasks.enums.MZTaskType; +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitTask; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.*; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.logging.Level; + +public class MZTaskScheduler implements MZTaskSchedulerInterface { + private final Plugin plugin; + private final ExecutorService asyncExecutor; + private final ScheduledExecutorService asyncScheduler; + private final ConcurrentHashMap tasks = new ConcurrentHashMap<>(); + + public MZTaskScheduler(Plugin plugin, int asyncThreads) { + this.plugin = plugin; + this.asyncExecutor = Executors.newFixedThreadPool(Math.max(1, asyncThreads), + r -> { + Thread t = new Thread(r, "MZCore-Async"); + t.setDaemon(true); + return t; + } + ); + this.asyncScheduler = Executors.newScheduledThreadPool(1, + r -> { + Thread t = new Thread(r, "MZCore-AsyncScheduled"); + t.setDaemon(true); + return t; + } + ); + } + + @Override + public MZTaskHandle runSync(Runnable task) { + UUID id = newId(MZTaskType.SYNC, MZTaskKind.RUN, "runSync"); + BukkitTask bt = Bukkit.getScheduler().runTask(plugin, () -> wrapOnce(id, task)); + return new BukkitHandle(id, bt, this::lookupInfo); + } + + @Override + public MZTaskHandle runAsync(Runnable task) { + UUID id = newId(MZTaskType.ASYNC, MZTaskKind.RUN, "runAsync"); + Future f = asyncExecutor.submit(() -> wrapOnce(id, task)); + return new FutureHandle(id, f, this::lookupInfo); + } + + @Override + public MZTaskHandle scheduleSync(Runnable task, long delay) { + UUID id = newId(MZTaskType.SYNC, MZTaskKind.SCHEDULE, "scheduleSync"); + BukkitTask bt = Bukkit.getScheduler().runTaskLater(plugin, () -> wrapOnce(id, task), delay); + return new BukkitHandle(id, bt, this::lookupInfo); + } + + @Override + public MZTaskHandle scheduleAsync(Runnable task, Duration delay) { + UUID id = newId(MZTaskType.ASYNC, MZTaskKind.SCHEDULE, "scheduleAsync"); + ScheduledFuture f = asyncScheduler.schedule(() -> wrapOnce(id, task), delay.toMillis(), TimeUnit.MILLISECONDS); + return new FutureHandle(id, f, this::lookupInfo); + } + + @Override + public MZTaskHandle repeatSync(Runnable task, long initialDelay, long period) { + UUID id = newId(MZTaskType.SYNC, MZTaskKind.REPEAT, "repeatSync"); + BukkitTask bt = Bukkit.getScheduler().runTaskTimer(plugin, () -> wrapRepeatTick(id, task), initialDelay, period); + return new BukkitHandle(id, bt, this::lookupInfo, true); + } + + @Override + public MZTaskHandle repeatAsync(Runnable task, Duration initialDelay, Duration period) { + UUID id = newId(MZTaskType.ASYNC, MZTaskKind.REPEAT, "repeatAsync"); + ScheduledFuture f = asyncScheduler.scheduleAtFixedRate(() -> wrapRepeatTick(id, task), + initialDelay.toMillis(), period.toMillis(), TimeUnit.MILLISECONDS); + return new FutureHandle(id, f, this::lookupInfo, true); + } + + @Override + public CompletableFuture supplyAsync(Supplier supplier) { + UUID id = newId(MZTaskType.ASYNC, MZTaskKind.SUPPLY, "supplyAsync"); + return CompletableFuture.supplyAsync(() -> { + markRunning(id); + try { + T val = supplier.get(); + markDone(id); + return val; + } catch (Exception e) { + markFailed(id, e); + throw new MZTaskException(e); + } + }, asyncExecutor); + } + + @Override + public void thenSync(CompletionStage completionStage, Consumer consumer) { + completionStage.whenComplete((val, err) -> + Bukkit.getScheduler().runTask(plugin, () -> { + if (err != null) throwUnchecked(unwrapCompletionException(err)); + consumer.accept(val); + }) + ); + } + + @Override + public CompletionStage withTimeout(CompletionStage stage, Duration timeout) { + Objects.requireNonNull(stage, "stage"); + Objects.requireNonNull(timeout, "timeout"); + CompletableFuture result = new CompletableFuture<>(); + stage.whenComplete((val, err) -> { + if (err != null) result.completeExceptionally(unwrapCompletionException(err)); + else result.complete(val); + }); + ScheduledFuture killer = asyncScheduler.schedule( + () -> result.completeExceptionally(new TimeoutException("Task timeout after " + timeout)), + timeout.toMillis(), TimeUnit.MILLISECONDS + ); + result.whenComplete((v, e) -> killer.cancel(false)); + return result; + } + + @Override + public Collection snapshot() { + return List.copyOf(tasks.values()); + } + + @Override + public void close() { + try { + Bukkit.getScheduler().cancelTasks(plugin); + } finally { + asyncScheduler.shutdownNow(); + asyncExecutor.shutdownNow(); + tasks.clear(); + } + } + + private UUID newId(MZTaskType type, MZTaskKind kind, String note) { + UUID id = UUID.randomUUID(); + long now = System.nanoTime(); + tasks.put(id, new MZTaskInfo(id, type, kind, MZTaskStatus.PENDING, now, null, 0, note, null)); + return id; + } + + private void wrapOnce(UUID id, Runnable r) { + markRunning(id); + try { + r.run(); + markDone(id); + } catch (Exception e) { + markFailed(id, e); + throwUnchecked(e); + } + } + + private void wrapRepeatTick(UUID id, Runnable r) { + markRunning(id); + try { + r.run(); + } catch (Exception e) { + markFailed(id, e); + plugin.getLogger().log(Level.WARNING, "[MZTaskScheduler] Task " + id + " failed", e); + } finally { + // entre deux exécutions + markWaiting(id); + } + } + private void markRunning(UUID id) { + tasks.computeIfPresent(id, (k, v) -> new MZTaskInfo(id, v.type(), v.kind(), MZTaskStatus.RUNNING, + v.start(), v.end(), v.runCount() + 1, v.note(), null)); + } + + private void markWaiting(UUID id) { + tasks.computeIfPresent(id, (k, v) -> new MZTaskInfo(id, v.type(), v.kind(), MZTaskStatus.WAITING, + v.start(), v.end(), v.runCount(), v.note(), v.error())); + } + + private void markDone(UUID id) { + long end = System.nanoTime(); + tasks.computeIfPresent(id, (k, v) -> new MZTaskInfo(id, v.type(), v.kind(), MZTaskStatus.COMPLETED, + v.start(), end, v.runCount(), v.note(), v.error())); + tasks.remove(id); + } + + private void markCancelled(UUID id) { + tasks.computeIfPresent(id, (k, v) -> new MZTaskInfo(id, v.type(), v.kind(), MZTaskStatus.CANCELLED, + v.start(), v.end(), v.runCount(), v.note(), v.error())); + } + + private void markFailed(UUID id, Exception e) { + long end = System.nanoTime(); + tasks.computeIfPresent(id, (k, v) -> new MZTaskInfo(id, v.type(), v.kind(), MZTaskStatus.FAILED, + v.start(), end, v.runCount(), v.note(), e)); + } + + private static void throwUnchecked(Throwable e) { + if (e instanceof RuntimeException re) throw re; + if (e instanceof Error er) throw er; + throw new MZTaskException(e); + } + + @Override + public Plugin getPlugin() { + return this.plugin; + } + + private MZTaskInfo lookupInfo(UUID id) { + return tasks.get(id); + } + + private final class FutureHandle implements MZTaskHandle { + private final UUID id; + private final Future future; + private final Function infoFn; + private final boolean repeating; + + FutureHandle(UUID id, Future future, Function infoFn) { + this(id, future, infoFn, false); + } + FutureHandle(UUID id, Future future, Function infoFn, boolean repeating) { + this.id = id; this.future = future; this.infoFn = infoFn; this.repeating = repeating; + } + + @Override public UUID getId() { return id; } + + @Override public boolean cancel() { + boolean ok = future.cancel(true); + markCancelled(id); + return ok; + } + + @Override public boolean isDone() { + MZTaskInfo ti = infoFn.apply(id); + if (ti == null) return future.isDone(); + return switch (ti.status()) { + case COMPLETED, FAILED, CANCELLED -> true; + default -> !repeating && future.isDone(); + }; + } + + @Override public boolean isCancelled() { + MZTaskInfo ti = infoFn.apply(id); + return ti != null && ti.status() == MZTaskStatus.CANCELLED || future.isCancelled(); + } + + @Override public Duration getDuration() { + MZTaskInfo ti = infoFn.apply(id); + return ti != null ? ti.duration() : Duration.ZERO; + } + + @Override public Throwable getCause() { + MZTaskInfo ti = infoFn.apply(id); + return ti != null ? ti.error() : null; + } + } + + private final class BukkitHandle implements MZTaskHandle { + private final UUID id; + private final BukkitTask task; + private final Function infoFn; + private final boolean repeating; + + BukkitHandle(UUID id, BukkitTask task, Function infoFn) { + this(id, task, infoFn, false); + } + BukkitHandle(UUID id, BukkitTask task, Function infoFn, boolean repeating) { + this.id = id; this.task = task; this.infoFn = infoFn; this.repeating = repeating; + } + + @Override public UUID getId() { return id; } + + @Override public boolean cancel() { + try { + task.cancel(); + return true; + } finally { + markCancelled(id); + } + } + + @Override public boolean isDone() { + MZTaskInfo ti = infoFn.apply(id); + if (ti == null) return task.isCancelled(); + return switch (ti.status()) { + case COMPLETED, FAILED, CANCELLED -> true; + default -> !repeating && (ti.end() != null); + }; + } + + @Override public boolean isCancelled() { + MZTaskInfo ti = infoFn.apply(id); + return (ti != null && ti.status() == MZTaskStatus.CANCELLED) || task.isCancelled(); + } + + @Override public Duration getDuration() { + MZTaskInfo ti = infoFn.apply(id); + return ti != null ? ti.duration() : Duration.ZERO; + } + + @Override public Throwable getCause() { + MZTaskInfo ti = infoFn.apply(id); + return ti != null ? ti.error() : null; + } + } +} diff --git a/src/main/java/fr/molzonas/mzcore/tasks/MZTaskSchedulerInterface.java b/src/main/java/fr/molzonas/mzcore/tasks/MZTaskSchedulerInterface.java new file mode 100644 index 0000000..c795c44 --- /dev/null +++ b/src/main/java/fr/molzonas/mzcore/tasks/MZTaskSchedulerInterface.java @@ -0,0 +1,41 @@ +package fr.molzonas.mzcore.tasks; + +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; + +import java.time.Duration; +import java.util.Collection; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public interface MZTaskSchedulerInterface extends AutoCloseable { + MZTaskHandle runSync(Runnable task); + MZTaskHandle runAsync(Runnable task); + MZTaskHandle scheduleSync(Runnable task, long delay); + MZTaskHandle scheduleAsync(Runnable task, Duration delay); + MZTaskHandle repeatSync(Runnable task, long initialDelay, long period); + MZTaskHandle repeatAsync(Runnable task, Duration delay, Duration period); + CompletableFuture supplyAsync(Supplier supplier); + void thenSync(CompletionStage completionStage, Consumer consumer); + default void thenSync(CompletionStage stage, Consumer ok, Consumer ko) { + stage.whenComplete((val, err) -> + Bukkit.getScheduler().runTask(getPlugin(), () -> { + if (err != null) ko.accept(unwrapCompletionException(err)); + else ok.accept(val); + }) + ); + } + default Throwable unwrapCompletionException(Throwable e) { + if (e instanceof CompletionException ce && ce.getCause() != null) return ce.getCause(); + if (e instanceof ExecutionException ee && ee.getCause() != null) return ee.getCause(); + return e; + } + CompletionStage withTimeout(CompletionStage stage, Duration timeout); + Collection snapshot(); + Plugin getPlugin(); + @Override void close(); +} diff --git a/src/main/java/fr/molzonas/mzcore/tasks/enums/MZTaskKind.java b/src/main/java/fr/molzonas/mzcore/tasks/enums/MZTaskKind.java new file mode 100644 index 0000000..0dee9fd --- /dev/null +++ b/src/main/java/fr/molzonas/mzcore/tasks/enums/MZTaskKind.java @@ -0,0 +1,5 @@ +package fr.molzonas.mzcore.tasks.enums; + +public enum MZTaskKind { + RUN, SCHEDULE, REPEAT, SUPPLY; +} diff --git a/src/main/java/fr/molzonas/mzcore/tasks/enums/MZTaskStatus.java b/src/main/java/fr/molzonas/mzcore/tasks/enums/MZTaskStatus.java new file mode 100644 index 0000000..563078b --- /dev/null +++ b/src/main/java/fr/molzonas/mzcore/tasks/enums/MZTaskStatus.java @@ -0,0 +1,5 @@ +package fr.molzonas.mzcore.tasks.enums; + +public enum MZTaskStatus { + PENDING, RUNNING, WAITING, COMPLETED, CANCELLED, FAILED, UNKNOWN; +} diff --git a/src/main/java/fr/molzonas/mzcore/tasks/enums/MZTaskType.java b/src/main/java/fr/molzonas/mzcore/tasks/enums/MZTaskType.java new file mode 100644 index 0000000..503ed31 --- /dev/null +++ b/src/main/java/fr/molzonas/mzcore/tasks/enums/MZTaskType.java @@ -0,0 +1,5 @@ +package fr.molzonas.mzcore.tasks.enums; + +public enum MZTaskType { + SYNC, ASYNC; +}