From 4c31c0d6e471f7f86d855a650f3d511ce8f1f86d Mon Sep 17 00:00:00 2001 From: Marc Baloup Date: Mon, 30 Jan 2023 23:49:40 +0100 Subject: [PATCH] Cron scheduler in Java, using the CronExpression library. --- .../lib/core/backup/BackupProcess.java | 7 +- .../lib/core/cron/CronScheduler.java | 179 ++++++++++++++++++ .../fr/pandacube/lib/core/cron/CronTask.java | 56 ++++++ 3 files changed, 237 insertions(+), 5 deletions(-) create mode 100644 pandalib-core/src/main/java/fr/pandacube/lib/core/cron/CronScheduler.java create mode 100644 pandalib-core/src/main/java/fr/pandacube/lib/core/cron/CronTask.java diff --git a/pandalib-core/src/main/java/fr/pandacube/lib/core/backup/BackupProcess.java b/pandalib-core/src/main/java/fr/pandacube/lib/core/backup/BackupProcess.java index c56ea5e..0641770 100644 --- a/pandalib-core/src/main/java/fr/pandacube/lib/core/backup/BackupProcess.java +++ b/pandalib-core/src/main/java/fr/pandacube/lib/core/backup/BackupProcess.java @@ -1,13 +1,12 @@ package fr.pandacube.lib.core.backup; import fc.cron.CronExpression; +import fr.pandacube.lib.core.cron.CronScheduler; import fr.pandacube.lib.util.FileUtils; import fr.pandacube.lib.util.Log; import net.md_5.bungee.api.ChatColor; import java.io.File; -import java.time.Instant; -import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; @@ -285,8 +284,6 @@ public abstract class BackupProcess implements Comparable, Runnab return 0; } - return parsedScheduling.nextTimeAfter(ZonedDateTime.ofInstant(Instant.ofEpochMilli(dirtySince), ZoneId.systemDefault())) - .toInstant() - .toEpochMilli(); + return CronScheduler.getNextTime(parsedScheduling, dirtySince); } } diff --git a/pandalib-core/src/main/java/fr/pandacube/lib/core/cron/CronScheduler.java b/pandalib-core/src/main/java/fr/pandacube/lib/core/cron/CronScheduler.java new file mode 100644 index 0000000..afe5002 --- /dev/null +++ b/pandalib-core/src/main/java/fr/pandacube/lib/core/cron/CronScheduler.java @@ -0,0 +1,179 @@ +package fr.pandacube.lib.core.cron; + +import fc.cron.CronExpression; +import fr.pandacube.lib.util.Log; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +// TODO Add support for persisted last execution timestamps +/** + * Application wide task scheduler using Cron expression. + */ +public class CronScheduler { + + private static final Object lock = new Object(); + + private static final List tasks = new ArrayList<>(); + private static final Map tasksById = new HashMap<>(); + + + + private static volatile boolean init = false; + private static void init() { + synchronized (CronScheduler.class) { + if (init) + return; + init = true; + loadLastRuns(); + Thread t = new Thread(CronScheduler::run, "Pandalib CronScheduler Thread"); + t.setDaemon(true); + t.start(); + } + } + + + + private static void run() { + synchronized (lock) { + for (;;) { + long wait = 0; + long now = System.currentTimeMillis(); + + if (!tasks.isEmpty()) { + CronTask next = tasks.get(0); + if (next.nextRun <= now) { + next.runAsync(); + setLastRun(next.taskId, next.nextRun); + onTaskUpdate(false); + continue; + } + else { + wait = next.nextRun - now; + } + } + try { + lock.wait(wait, 0); + } catch (InterruptedException e) { + return; + } + } + } + } + + + /** + * Schedule a task. + * @param taskId the id of the task. + * @param cronExpression the scheduling of the task. May use seconds (6 values) or not (5 values) + * @param task the task to run. + */ + public static void schedule(String taskId, String cronExpression, Runnable task) { + init(); + synchronized (lock) { + long lastRun = getLastRun(taskId); + + CronTask existing = getTask(taskId); + if (existing != null) { + // replacing task + removeTask(taskId); + } + + CronExpression cron = new CronExpression(cronExpression, cronExpression.split("\\s+").length == 6); + addTask(new CronTask(taskId, task, cron, lastRun)); + onTaskUpdate(true); + } + } + + /** + * Cancel a scheduled task. + * Will not stop a current execution of the task. If the task does not exists, it will not do anything. + * @param taskId the id of the task to cancel. + */ + public static void unSchedule(String taskId) { + synchronized (lock) { + CronTask existing = getTask(taskId); + if (existing != null) { + removeTask(taskId); + onTaskUpdate(true); + } + } + } + + + + private static void onTaskUpdate(boolean notify) { + synchronized (lock) { + tasks.sort(Comparator.comparing(t -> t.nextRun)); + if (notify) { + Log.info("Scheduler notified."); + lock.notify(); + } + } + } + + + private static void addTask(CronTask nextTask) { + synchronized (lock) { + tasks.add(nextTask); + tasksById.put(nextTask.taskId, nextTask); + } + } + + private static CronTask getTask(String taskId) { + synchronized (lock) { + return tasksById.get(taskId); + } + } + + private static void removeTask(String taskId) { + synchronized (lock) { + tasks.remove(tasksById.remove(taskId)); + } + } + + + + + + private static final Map savedLastRun = new LinkedHashMap<>(); + + private static void saveLastRuns() { + // TODO + } + + private static void loadLastRuns() { + // TODO + } + + /* package */ static void setLastRun(String taskId, long lastRun) { + savedLastRun.put(taskId, lastRun); + saveLastRuns(); + } + + private static long getLastRun(String taskId) { + return savedLastRun.getOrDefault(taskId, System.currentTimeMillis()); + } + + + /** + * Tells when the next time is scheduled, according to the provided cron expression, strictly after the provided time. + * @param expr the cron expression to use to determine the schedule time. + * @param lastTime the start search time. + * @return the time of the next execution of the task. + */ + public static long getNextTime(CronExpression expr, long lastTime) { + return expr.nextTimeAfter(ZonedDateTime.ofInstant(Instant.ofEpochMilli(lastTime), ZoneId.systemDefault())) + .toInstant() + .toEpochMilli(); + } + + +} diff --git a/pandalib-core/src/main/java/fr/pandacube/lib/core/cron/CronTask.java b/pandalib-core/src/main/java/fr/pandacube/lib/core/cron/CronTask.java new file mode 100644 index 0000000..0c32f92 --- /dev/null +++ b/pandalib-core/src/main/java/fr/pandacube/lib/core/cron/CronTask.java @@ -0,0 +1,56 @@ +package fr.pandacube.lib.core.cron; + +import fc.cron.CronExpression; + +/* package */ class CronTask { + /** + * The id of the task, used to persist its last run. + */ + /* package */ final String taskId; + /** + * The task to run. + */ + /* package */ final Runnable task; + /** + * The cron expression telling when to run the task. + */ + /* package */ final CronExpression scheduling; + /** + * Millis timestamp of the previous run. Must be saved. + */ + /* package */ long lastRun; + /** + * Millis timestamp of the next run. + */ + /* package */ long nextRun; + + + + /* package */ CronTask(String taskId, Runnable task, CronExpression scheduling, long lastRun) { + this.taskId = taskId; + this.task = task; + this.scheduling = scheduling; + this.lastRun = lastRun; + updateNextRun(); + } + + + + /* package */ void updateNextRun() { + nextRun = CronScheduler.getNextTime(scheduling, lastRun); + } + + + /* package */ void runAsync() { + Thread t = new Thread(task, "Pandalib CronTask " + taskId); + t.start(); + lastRun = nextRun; + updateNextRun(); + } + + + + + + +}