From 27dce051f7c76e993ec3b4cdd8c2fc3b7ba51fca Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Fri, 6 Aug 2021 01:12:29 +0200 Subject: [PATCH] Improved scheduling and test reliability. --- pom.xml | 33 ++++++++++++++++-- src/main/java/module-info.java | 1 + .../simply_scheduled/BasicScheduler.java | 19 ++++++++--- .../nl/andrewlalis/simply_scheduled/Demo.java | 5 ++- .../simply_scheduled/Scheduler.java | 17 ++++++++++ .../schedule/MinutelySchedule.java | 32 ----------------- .../schedule/RepeatingSchedule.java | 27 ++++++++++----- .../simply_scheduled/schedule/Task.java | 24 +++++++++++-- ...dulerTest.java => BasicSchedulerTest.java} | 34 +++++++++++-------- 9 files changed, 123 insertions(+), 69 deletions(-) delete mode 100644 src/main/java/nl/andrewlalis/simply_scheduled/schedule/MinutelySchedule.java rename src/test/java/nl/andrewlalis/simply_scheduled/{SchedulerTest.java => BasicSchedulerTest.java} (71%) diff --git a/pom.xml b/pom.xml index 48dbd2b..0adf674 100644 --- a/pom.xml +++ b/pom.xml @@ -7,6 +7,33 @@ nl.andrewlalis simply-scheduled 1.0-SNAPSHOT + jar + + Simply Scheduled + Lightweight task scheduling library. + https://github.com/andrewlalis/SimplyScheduled + + + + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + Andrew Lalis + andrewlalisofficial@gmail.com + CEST + + + + + scm:git:git://github.com/andrewlalis/SimplyScheduled.git + scm:git:ssh://github.com:andrewlalis/SimplyScheduled.git + https://github.com/andrewlalis/SimplyScheduled + UTF-8 @@ -20,7 +47,7 @@ org.apache.maven.plugins maven-surefire-plugin - 2.22.2 + 3.0.0-M5 @@ -30,7 +57,7 @@ org.junit junit-bom - 5.7.1 + 5.7.2 pom import @@ -42,7 +69,7 @@ org.junit.jupiter junit-jupiter - 5.7.1 + 5.7.2 test diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 5f23a1c..5735d78 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,5 +1,6 @@ module simply_scheduled { exports nl.andrewlalis.simply_scheduled; exports nl.andrewlalis.simply_scheduled.schedule; + opens nl.andrewlalis.simply_scheduled; } \ No newline at end of file diff --git a/src/main/java/nl/andrewlalis/simply_scheduled/BasicScheduler.java b/src/main/java/nl/andrewlalis/simply_scheduled/BasicScheduler.java index a1d1391..49517e1 100644 --- a/src/main/java/nl/andrewlalis/simply_scheduled/BasicScheduler.java +++ b/src/main/java/nl/andrewlalis/simply_scheduled/BasicScheduler.java @@ -8,6 +8,7 @@ import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.RejectedExecutionException; /** * A simple thread-based scheduler that sleeps until the next task, runs it @@ -47,9 +48,9 @@ public class BasicScheduler extends Thread implements Scheduler { this.running = true; while (this.running) { try { - Task nextTask = this.tasks.take(); - Instant now = this.clock.instant(); - Optional optionalNextExecution = nextTask.getSchedule().getNextExecutionTime(now); + final Task nextTask = this.tasks.take(); + final Instant now = this.clock.instant(); + final Optional optionalNextExecution = nextTask.getSchedule().getNextExecutionTime(now); if (optionalNextExecution.isEmpty()) { continue; // Skip if the schedule doesn't have a next execution planned. } @@ -57,10 +58,18 @@ public class BasicScheduler extends Thread implements Scheduler { if (waitTime > 0) { Thread.sleep(waitTime); } - this.executorService.execute(nextTask.getRunnable()); + try { + this.executorService.execute(nextTask.getRunnable()); + } catch (RejectedExecutionException e) { + if (!this.executorService.isShutdown()) { + // Only show the stack trace if the executor service is not being shut down. + // We expect the service to reject executions if it is shutting down. + e.printStackTrace(); + } + } nextTask.getSchedule().markExecuted(this.clock.instant()); if (nextTask.getSchedule().isRepeating()) { - this.tasks.put(nextTask); // Put the task back in the queue. + this.tasks.put(nextTask); } } catch (InterruptedException e) { this.setRunning(false); diff --git a/src/main/java/nl/andrewlalis/simply_scheduled/Demo.java b/src/main/java/nl/andrewlalis/simply_scheduled/Demo.java index 73afa77..bab9205 100644 --- a/src/main/java/nl/andrewlalis/simply_scheduled/Demo.java +++ b/src/main/java/nl/andrewlalis/simply_scheduled/Demo.java @@ -2,7 +2,6 @@ package nl.andrewlalis.simply_scheduled; import nl.andrewlalis.simply_scheduled.schedule.RepeatingSchedule; import nl.andrewlalis.simply_scheduled.schedule.Schedule; -import nl.andrewlalis.simply_scheduled.schedule.Task; import java.time.Instant; import java.time.temporal.ChronoUnit; @@ -10,9 +9,9 @@ import java.time.temporal.ChronoUnit; public class Demo { public static void main(String[] args) { Scheduler scheduler = new BasicScheduler(); - Schedule schedule = new RepeatingSchedule(ChronoUnit.MILLIS, 250); + Schedule schedule = new RepeatingSchedule(ChronoUnit.SECONDS, 5, Instant.now().truncatedTo(ChronoUnit.SECONDS)); Runnable job = () -> System.out.println("Doing task: " + Instant.now().toString()); - scheduler.addTask(Task.of(job, schedule)); + scheduler.addTask(job, schedule); scheduler.start(); } } diff --git a/src/main/java/nl/andrewlalis/simply_scheduled/Scheduler.java b/src/main/java/nl/andrewlalis/simply_scheduled/Scheduler.java index c948874..ff253b3 100644 --- a/src/main/java/nl/andrewlalis/simply_scheduled/Scheduler.java +++ b/src/main/java/nl/andrewlalis/simply_scheduled/Scheduler.java @@ -1,5 +1,6 @@ package nl.andrewlalis.simply_scheduled; +import nl.andrewlalis.simply_scheduled.schedule.Schedule; import nl.andrewlalis.simply_scheduled.schedule.Task; /** @@ -14,6 +15,15 @@ public interface Scheduler { */ void addTask(Task task); + /** + * Adds a task to this scheduler. + * @param runnable The code to run. + * @param schedule The schedule that dictates when the code should run. + */ + default void addTask(Runnable runnable, Schedule schedule) { + addTask(new Task(runnable, schedule)); + } + /** * Starts the scheduler. A scheduler should only execute tasks once it has * started, and it is up to the implementation to determine whether new @@ -27,4 +37,11 @@ public interface Scheduler { * any currently-executing tasks are immediately shutdown. */ void stop(boolean force); + + /** + * Stops the scheduler, and waits for any currently-executing tasks to finish. + */ + default void stop() { + stop(false); + } } diff --git a/src/main/java/nl/andrewlalis/simply_scheduled/schedule/MinutelySchedule.java b/src/main/java/nl/andrewlalis/simply_scheduled/schedule/MinutelySchedule.java deleted file mode 100644 index 4117cc8..0000000 --- a/src/main/java/nl/andrewlalis/simply_scheduled/schedule/MinutelySchedule.java +++ /dev/null @@ -1,32 +0,0 @@ -package nl.andrewlalis.simply_scheduled.schedule; - -import java.time.Instant; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.temporal.ChronoUnit; -import java.util.Optional; - -public class MinutelySchedule implements Schedule { - - private final int second; - private final ZoneId zoneId; - - public MinutelySchedule(int second, ZoneId zoneId) { - this.second = second; - this.zoneId = zoneId; - } - - public MinutelySchedule(int second) { - this(second, ZoneId.systemDefault()); - } - - @Override - public Optional getNextExecutionTime(Instant referenceInstant) { - ZonedDateTime currentTime = referenceInstant.atZone(this.zoneId); - int currentSecond = currentTime.getSecond(); - if (currentSecond >= this.second) { - return Optional.of(currentTime.plusMinutes(1).withSecond(this.second).truncatedTo(ChronoUnit.SECONDS).toInstant()); - } - return Optional.of(currentTime.withSecond(this.second).truncatedTo(ChronoUnit.SECONDS).toInstant()); - } -} diff --git a/src/main/java/nl/andrewlalis/simply_scheduled/schedule/RepeatingSchedule.java b/src/main/java/nl/andrewlalis/simply_scheduled/schedule/RepeatingSchedule.java index 00c1057..1f1cb8c 100644 --- a/src/main/java/nl/andrewlalis/simply_scheduled/schedule/RepeatingSchedule.java +++ b/src/main/java/nl/andrewlalis/simply_scheduled/schedule/RepeatingSchedule.java @@ -11,17 +11,29 @@ import java.util.Optional; public class RepeatingSchedule implements Schedule { private final ChronoUnit unit; private final long multiple; - private Instant lastExecution; + private long elapsedIntervals = 0; + private final Instant start; /** * Constructs a new repeating schedule. * @param unit The unit of time that the interval consists of. - * @param multiple The + * @param multiple The number of units of time that each interval consists of. + * @param start The starting point for this schedule. */ - public RepeatingSchedule(ChronoUnit unit, long multiple) { + public RepeatingSchedule(ChronoUnit unit, long multiple, Instant start) { this.unit = unit; this.multiple = multiple; - this.lastExecution = null; + this.start = start; + } + + /** + * Constructs a new repeating schedule, using {@link Instant#now()} as the + * starting point. + * @param unit The unit of time that the interval consists of. + * @param multiple The number of units of time that each interval consists of. + */ + public RepeatingSchedule(ChronoUnit unit, long multiple) { + this(unit, multiple, Instant.now()); } /** @@ -32,14 +44,11 @@ public class RepeatingSchedule implements Schedule { */ @Override public Optional getNextExecutionTime(Instant referenceInstant) { - if (this.lastExecution == null) { - this.lastExecution = referenceInstant; - } - return Optional.of(this.lastExecution.plus(multiple, unit)); + return Optional.of(this.start.plus(elapsedIntervals * multiple, unit)); } @Override public void markExecuted(Instant instant) { - this.lastExecution = instant; + this.elapsedIntervals++; } } diff --git a/src/main/java/nl/andrewlalis/simply_scheduled/schedule/Task.java b/src/main/java/nl/andrewlalis/simply_scheduled/schedule/Task.java index aa821fb..b563a81 100644 --- a/src/main/java/nl/andrewlalis/simply_scheduled/schedule/Task.java +++ b/src/main/java/nl/andrewlalis/simply_scheduled/schedule/Task.java @@ -14,20 +14,40 @@ public class Task implements Comparable{ private final Runnable runnable; private final Schedule schedule; + /** + * Constructs a new task that will run the given runnable according to the + * given schedule. Allows for specifying a {@link Clock}, this is mostly + * useful for testing purposes. + * @param clock The clock to use for time-sensitive operations. + * @param runnable The code to run when the task is executed. + * @param schedule The schedule which determines when the task is executed. + */ public Task(Clock clock, Runnable runnable, Schedule schedule) { this.clock = clock; this.runnable = runnable; this.schedule = schedule; } - public static Task of(Runnable runnable, Schedule schedule) { - return new Task(Clock.systemDefaultZone(), runnable, schedule); + /** + * Constructs a new task that will run the given runnable according to the + * given schedule. + * @param runnable The code to run when the task is executed. + * @param schedule The schedule which determines when the task is executed. + */ + public Task(Runnable runnable, Schedule schedule) { + this(Clock.systemDefaultZone(), runnable, schedule); } + /** + * @return The runnable which will be executed when this task is scheduled. + */ public Runnable getRunnable() { return runnable; } + /** + * @return The schedule for this task. + */ public Schedule getSchedule() { return schedule; } diff --git a/src/test/java/nl/andrewlalis/simply_scheduled/SchedulerTest.java b/src/test/java/nl/andrewlalis/simply_scheduled/BasicSchedulerTest.java similarity index 71% rename from src/test/java/nl/andrewlalis/simply_scheduled/SchedulerTest.java rename to src/test/java/nl/andrewlalis/simply_scheduled/BasicSchedulerTest.java index ef510f5..7f7f478 100644 --- a/src/test/java/nl/andrewlalis/simply_scheduled/SchedulerTest.java +++ b/src/test/java/nl/andrewlalis/simply_scheduled/BasicSchedulerTest.java @@ -1,6 +1,5 @@ package nl.andrewlalis.simply_scheduled; -import nl.andrewlalis.simply_scheduled.schedule.MinutelySchedule; import nl.andrewlalis.simply_scheduled.schedule.RepeatingSchedule; import nl.andrewlalis.simply_scheduled.schedule.Schedule; import nl.andrewlalis.simply_scheduled.schedule.Task; @@ -8,6 +7,7 @@ import org.junit.jupiter.api.Test; import java.time.Clock; import java.time.Instant; +import java.time.LocalDateTime; import java.time.ZoneOffset; import java.time.temporal.ChronoUnit; import java.util.concurrent.Executors; @@ -16,7 +16,11 @@ import java.util.concurrent.atomic.AtomicInteger; import static org.junit.jupiter.api.Assertions.*; -public class SchedulerTest { +/** + * Tests the functionality of the {@link BasicScheduler} to reliably execute + * scheduled tasks. + */ +public class BasicSchedulerTest { @Test void testSchedule() { @@ -28,7 +32,7 @@ public class SchedulerTest { flag.set(true); System.out.println("\tExecuted task."); }; - Task task = new Task(clock, taskRunnable, new MinutelySchedule(3)); + Task task = new Task(clock, taskRunnable, new RepeatingSchedule(ChronoUnit.SECONDS, 2, clock.instant().plusSeconds(1))); scheduler.addTask(task); scheduler.start(); System.out.println("Now: " + clock.instant().toString()); @@ -45,24 +49,24 @@ public class SchedulerTest { } @Test - void testRepeatingSchedule() { - Scheduler scheduler = new BasicScheduler(Clock.systemUTC(), Executors.newWorkStealingPool()); - Schedule schedule = new RepeatingSchedule(ChronoUnit.SECONDS, 1); + void testRepeatingSchedule() throws InterruptedException { + Scheduler scheduler = new BasicScheduler(Clock.systemUTC(), Executors.newFixedThreadPool(3)); + Instant startInstant = Instant.now(Clock.systemUTC()); + Schedule schedule = new RepeatingSchedule(ChronoUnit.SECONDS, 1, startInstant); AtomicInteger value = new AtomicInteger(0); Runnable taskRunnable = () -> { value.set(value.get() + 1); - System.out.println("\tExecuted task."); + System.out.println("\tExecuted task at " + LocalDateTime.now()); }; - scheduler.addTask(Task.of(taskRunnable, schedule)); + scheduler.addTask(new Task(taskRunnable, schedule)); assertEquals(0, value.get()); scheduler.start(); - System.out.println("Waiting 3.5 seconds for 3 iterations."); - try { - Thread.sleep(3500); - } catch (InterruptedException e) { - e.printStackTrace(); - } - assertEquals(3, value.get()); + + Thread.sleep(3500); + // We expect the task to have run 4 times: + // at t=0, t=1, t=2, and t=3. + assertEquals(4, value.get()); + scheduler.stop(true); } }