Cron scheduler in Java, using the CronExpression library.

This commit is contained in:
Marc Baloup 2023-01-30 23:49:40 +01:00
parent c5c6181e15
commit 4c31c0d6e4
Signed by: marcbal
GPG Key ID: BBC0FE3ABC30B893
3 changed files with 237 additions and 5 deletions

View File

@ -1,13 +1,12 @@
package fr.pandacube.lib.core.backup; package fr.pandacube.lib.core.backup;
import fc.cron.CronExpression; import fc.cron.CronExpression;
import fr.pandacube.lib.core.cron.CronScheduler;
import fr.pandacube.lib.util.FileUtils; import fr.pandacube.lib.util.FileUtils;
import fr.pandacube.lib.util.Log; import fr.pandacube.lib.util.Log;
import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.ChatColor;
import java.io.File; import java.io.File;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder; import java.time.format.DateTimeFormatterBuilder;
@ -285,8 +284,6 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab
return 0; return 0;
} }
return parsedScheduling.nextTimeAfter(ZonedDateTime.ofInstant(Instant.ofEpochMilli(dirtySince), ZoneId.systemDefault())) return CronScheduler.getNextTime(parsedScheduling, dirtySince);
.toInstant()
.toEpochMilli();
} }
} }

View File

@ -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<CronTask> tasks = new ArrayList<>();
private static final Map<String, CronTask> 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<String, Long> 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();
}
}

View File

@ -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();
}
}