From 020d8e2258692cddb1f2e465bb535b2fbd09695a Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Sat, 17 Apr 2021 12:51:23 +0200 Subject: [PATCH] Added demo and updated some javadoc and some mechanics. --- .../simply_scheduled/BasicScheduler.java | 25 +++++++++--- .../nl/andrewlalis/simply_scheduled/Demo.java | 18 +++++++++ .../schedule/DailySchedule.java | 7 ++-- .../schedule/HourlySchedule.java | 7 ++-- .../schedule/MinutelySchedule.java | 7 ++-- .../schedule/RepeatingSchedule.java | 5 ++- .../simply_scheduled/schedule/Schedule.java | 15 +++++++- .../schedule/SpecificInstantSchedule.java | 38 +++++++++++++++++++ .../simply_scheduled/schedule/Task.java | 14 +++++-- .../simply_scheduled/SchedulerTest.java | 8 ++-- 10 files changed, 119 insertions(+), 25 deletions(-) create mode 100644 src/main/java/nl/andrewlalis/simply_scheduled/Demo.java create mode 100644 src/main/java/nl/andrewlalis/simply_scheduled/schedule/SpecificInstantSchedule.java diff --git a/src/main/java/nl/andrewlalis/simply_scheduled/BasicScheduler.java b/src/main/java/nl/andrewlalis/simply_scheduled/BasicScheduler.java index 32df530..a1d1391 100644 --- a/src/main/java/nl/andrewlalis/simply_scheduled/BasicScheduler.java +++ b/src/main/java/nl/andrewlalis/simply_scheduled/BasicScheduler.java @@ -4,6 +4,7 @@ import nl.andrewlalis.simply_scheduled.schedule.Task; import java.time.Clock; import java.time.Instant; +import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.PriorityBlockingQueue; @@ -18,18 +19,26 @@ public class BasicScheduler extends Thread implements Scheduler { private final ExecutorService executorService; private boolean running = false; - public BasicScheduler(Clock clock) { + public BasicScheduler(Clock clock, ExecutorService executorService) { this.clock = clock; this.tasks = new PriorityBlockingQueue<>(); - this.executorService = Executors.newWorkStealingPool(); + this.executorService = executorService; } public BasicScheduler() { - this(Clock.systemDefaultZone()); + this(Clock.systemDefaultZone(), Executors.newWorkStealingPool()); } + /** + * Adds a task to this scheduler's queue. + * @param task The task to add. + * @throws RuntimeException If a task is added while the scheduler is running. + */ @Override public void addTask(Task task) { + if (this.running) { + throw new RuntimeException("Cannot add tasks to the basic scheduler while it is running."); + } this.tasks.add(task); } @@ -40,13 +49,19 @@ public class BasicScheduler extends Thread implements Scheduler { try { Task nextTask = this.tasks.take(); Instant now = this.clock.instant(); - long waitTime = nextTask.getSchedule().getNextExecutionTime(now).toEpochMilli() - now.toEpochMilli(); + Optional optionalNextExecution = nextTask.getSchedule().getNextExecutionTime(now); + if (optionalNextExecution.isEmpty()) { + continue; // Skip if the schedule doesn't have a next execution planned. + } + long waitTime = optionalNextExecution.get().toEpochMilli() - now.toEpochMilli(); if (waitTime > 0) { Thread.sleep(waitTime); } this.executorService.execute(nextTask.getRunnable()); nextTask.getSchedule().markExecuted(this.clock.instant()); - this.tasks.put(nextTask); // Put the task back in the queue. + if (nextTask.getSchedule().isRepeating()) { + this.tasks.put(nextTask); // Put the task back in the queue. + } } 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 new file mode 100644 index 0000000..73afa77 --- /dev/null +++ b/src/main/java/nl/andrewlalis/simply_scheduled/Demo.java @@ -0,0 +1,18 @@ +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; + +public class Demo { + public static void main(String[] args) { + Scheduler scheduler = new BasicScheduler(); + Schedule schedule = new RepeatingSchedule(ChronoUnit.MILLIS, 250); + Runnable job = () -> System.out.println("Doing task: " + Instant.now().toString()); + scheduler.addTask(Task.of(job, schedule)); + scheduler.start(); + } +} diff --git a/src/main/java/nl/andrewlalis/simply_scheduled/schedule/DailySchedule.java b/src/main/java/nl/andrewlalis/simply_scheduled/schedule/DailySchedule.java index 0a5d8cd..9e61d60 100644 --- a/src/main/java/nl/andrewlalis/simply_scheduled/schedule/DailySchedule.java +++ b/src/main/java/nl/andrewlalis/simply_scheduled/schedule/DailySchedule.java @@ -1,6 +1,7 @@ package nl.andrewlalis.simply_scheduled.schedule; import java.time.*; +import java.util.Optional; /** * A daily schedule plans for the execution of a task once per day, at a @@ -20,13 +21,13 @@ public class DailySchedule implements Schedule { } @Override - public Instant getNextExecutionTime(Instant referenceInstant) { + public Optional getNextExecutionTime(Instant referenceInstant) { ZonedDateTime currentTime = referenceInstant.atZone(this.zoneId); LocalDate currentDay = LocalDate.from(referenceInstant); ZonedDateTime sameDayExecution = currentDay.atTime(this.time).atZone(this.zoneId); if (sameDayExecution.isBefore(currentTime)) { - return sameDayExecution.toInstant(); + return Optional.of(sameDayExecution.toInstant()); } - return sameDayExecution.plusDays(1).toInstant(); + return Optional.of(sameDayExecution.plusDays(1).toInstant()); } } diff --git a/src/main/java/nl/andrewlalis/simply_scheduled/schedule/HourlySchedule.java b/src/main/java/nl/andrewlalis/simply_scheduled/schedule/HourlySchedule.java index 5d70ea4..5db9b1e 100644 --- a/src/main/java/nl/andrewlalis/simply_scheduled/schedule/HourlySchedule.java +++ b/src/main/java/nl/andrewlalis/simply_scheduled/schedule/HourlySchedule.java @@ -4,6 +4,7 @@ import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; +import java.util.Optional; /** * An hourly schedule is used to execute a task once per hour, at a specific @@ -26,12 +27,12 @@ public class HourlySchedule implements Schedule { } @Override - public Instant getNextExecutionTime(Instant referenceInstant) { + public Optional getNextExecutionTime(Instant referenceInstant) { ZonedDateTime currentTime = referenceInstant.atZone(this.zoneId); int currentMinute = currentTime.getMinute(); if (currentMinute < this.minute) { - return currentTime.truncatedTo(ChronoUnit.MINUTES).plusMinutes(this.minute - currentMinute).toInstant(); + return Optional.of(currentTime.truncatedTo(ChronoUnit.MINUTES).plusMinutes(this.minute - currentMinute).toInstant()); } - return currentTime.plusHours(1).plusMinutes(this.minute).truncatedTo(ChronoUnit.MINUTES).toInstant(); + return Optional.of(currentTime.plusHours(1).plusMinutes(this.minute).truncatedTo(ChronoUnit.MINUTES).toInstant()); } } diff --git a/src/main/java/nl/andrewlalis/simply_scheduled/schedule/MinutelySchedule.java b/src/main/java/nl/andrewlalis/simply_scheduled/schedule/MinutelySchedule.java index 0afe259..4117cc8 100644 --- a/src/main/java/nl/andrewlalis/simply_scheduled/schedule/MinutelySchedule.java +++ b/src/main/java/nl/andrewlalis/simply_scheduled/schedule/MinutelySchedule.java @@ -4,6 +4,7 @@ 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 { @@ -20,12 +21,12 @@ public class MinutelySchedule implements Schedule { } @Override - public Instant getNextExecutionTime(Instant referenceInstant) { + public Optional getNextExecutionTime(Instant referenceInstant) { ZonedDateTime currentTime = referenceInstant.atZone(this.zoneId); int currentSecond = currentTime.getSecond(); if (currentSecond >= this.second) { - return currentTime.plusMinutes(1).withSecond(this.second).truncatedTo(ChronoUnit.SECONDS).toInstant(); + return Optional.of(currentTime.plusMinutes(1).withSecond(this.second).truncatedTo(ChronoUnit.SECONDS).toInstant()); } - return currentTime.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 284b381..00c1057 100644 --- a/src/main/java/nl/andrewlalis/simply_scheduled/schedule/RepeatingSchedule.java +++ b/src/main/java/nl/andrewlalis/simply_scheduled/schedule/RepeatingSchedule.java @@ -2,6 +2,7 @@ package nl.andrewlalis.simply_scheduled.schedule; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.Optional; /** * A schedule which repeatedly executes a task at a regular interval specified @@ -30,11 +31,11 @@ public class RepeatingSchedule implements Schedule { * @return The next instant to execute the task at. */ @Override - public Instant getNextExecutionTime(Instant referenceInstant) { + public Optional getNextExecutionTime(Instant referenceInstant) { if (this.lastExecution == null) { this.lastExecution = referenceInstant; } - return this.lastExecution.plus(multiple, unit); + return Optional.of(this.lastExecution.plus(multiple, unit)); } @Override diff --git a/src/main/java/nl/andrewlalis/simply_scheduled/schedule/Schedule.java b/src/main/java/nl/andrewlalis/simply_scheduled/schedule/Schedule.java index 726ca78..e1b5a5c 100644 --- a/src/main/java/nl/andrewlalis/simply_scheduled/schedule/Schedule.java +++ b/src/main/java/nl/andrewlalis/simply_scheduled/schedule/Schedule.java @@ -3,6 +3,7 @@ package nl.andrewlalis.simply_scheduled.schedule; import nl.andrewlalis.simply_scheduled.Scheduler; import java.time.Instant; +import java.util.Optional; /** * A schedule is used by a {@link Scheduler} to determine how long to wait for @@ -16,9 +17,10 @@ public interface Schedule { * * @param referenceInstant The instant representing the current time. * @return An instant in the future indicating the next time at which a task - * using this schedule should be executed. + * using this schedule should be executed. If the optional is empty, it + * indicates that there are no planned execution times. */ - Instant getNextExecutionTime(Instant referenceInstant); + Optional getNextExecutionTime(Instant referenceInstant); /** * This method is called on the schedule as an indication that the scheduler @@ -28,4 +30,13 @@ public interface Schedule { default void markExecuted(Instant instant) { // Default no-op. } + + /** + * Tells whether tasks executed in accordance with this schedule should be + * re-queued again for another execution. Defaults to true. + * @return True if this schedule is repeating, or false otherwise. + */ + default boolean isRepeating() { + return true; + } } diff --git a/src/main/java/nl/andrewlalis/simply_scheduled/schedule/SpecificInstantSchedule.java b/src/main/java/nl/andrewlalis/simply_scheduled/schedule/SpecificInstantSchedule.java new file mode 100644 index 0000000..57b3bc9 --- /dev/null +++ b/src/main/java/nl/andrewlalis/simply_scheduled/schedule/SpecificInstantSchedule.java @@ -0,0 +1,38 @@ +package nl.andrewlalis.simply_scheduled.schedule; + +import java.time.Instant; +import java.time.ZonedDateTime; +import java.util.*; +import java.util.stream.Collectors; + +/** + * A schedule in which a discrete number of execution times are defined at + * initialization. + */ +public class SpecificInstantSchedule implements Schedule { + private final List executionTimes; + + public SpecificInstantSchedule(Instant... executionTimes) { + this(new ArrayList<>(Arrays.asList(executionTimes))); + } + + public SpecificInstantSchedule(ZonedDateTime... zonedDateTimes) { + this(Arrays.stream(zonedDateTimes).map(ZonedDateTime::toInstant).collect(Collectors.toList())); + } + + private SpecificInstantSchedule(List executionTimes) { + this.executionTimes = executionTimes; + Collections.sort(this.executionTimes); + } + + @Override + public Optional getNextExecutionTime(Instant referenceInstant) { + while (!this.executionTimes.isEmpty()) { + Instant nextExecutionTime = this.executionTimes.remove(0); + if (nextExecutionTime.isBefore(referenceInstant)) { + return Optional.of(nextExecutionTime); + } + } + return Optional.empty(); + } +} 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 e0baf11..aa821fb 100644 --- a/src/main/java/nl/andrewlalis/simply_scheduled/schedule/Task.java +++ b/src/main/java/nl/andrewlalis/simply_scheduled/schedule/Task.java @@ -2,6 +2,7 @@ package nl.andrewlalis.simply_scheduled.schedule; import java.time.Clock; import java.time.Instant; +import java.util.Optional; /** * A task consists of a runnable job, and a schedule that defines precisely when @@ -19,8 +20,8 @@ public class Task implements Comparable{ this.schedule = schedule; } - public Task(Runnable runnable, Schedule schedule) { - this(Clock.systemDefaultZone(), runnable, schedule); + public static Task of(Runnable runnable, Schedule schedule) { + return new Task(Clock.systemDefaultZone(), runnable, schedule); } public Runnable getRunnable() { @@ -34,6 +35,13 @@ public class Task implements Comparable{ @Override public int compareTo(Task o) { Instant now = clock.instant(); - return this.schedule.getNextExecutionTime(now).compareTo(o.getSchedule().getNextExecutionTime(now)); + Optional t1 = this.schedule.getNextExecutionTime(now); + Optional t2 = o.getSchedule().getNextExecutionTime(now); + + if (t1.isEmpty() && t2.isEmpty()) return 0; + if (t1.isPresent() && t2.isEmpty()) return 1; + if (t1.isEmpty()) return -1; + + return t1.get().compareTo(t2.get()); } } diff --git a/src/test/java/nl/andrewlalis/simply_scheduled/SchedulerTest.java b/src/test/java/nl/andrewlalis/simply_scheduled/SchedulerTest.java index 7635b6f..ef510f5 100644 --- a/src/test/java/nl/andrewlalis/simply_scheduled/SchedulerTest.java +++ b/src/test/java/nl/andrewlalis/simply_scheduled/SchedulerTest.java @@ -10,6 +10,7 @@ import java.time.Clock; import java.time.Instant; import java.time.ZoneOffset; import java.time.temporal.ChronoUnit; +import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -20,7 +21,7 @@ public class SchedulerTest { @Test void testSchedule() { Clock clock = Clock.fixed(Instant.now().truncatedTo(ChronoUnit.MINUTES), ZoneOffset.UTC); - Scheduler scheduler = new BasicScheduler(clock); + Scheduler scheduler = new BasicScheduler(clock, Executors.newSingleThreadExecutor()); int secondsLeft = 4; AtomicBoolean flag = new AtomicBoolean(false); Runnable taskRunnable = () -> { @@ -45,15 +46,14 @@ public class SchedulerTest { @Test void testRepeatingSchedule() { - Scheduler scheduler = new BasicScheduler(); + Scheduler scheduler = new BasicScheduler(Clock.systemUTC(), Executors.newWorkStealingPool()); Schedule schedule = new RepeatingSchedule(ChronoUnit.SECONDS, 1); AtomicInteger value = new AtomicInteger(0); Runnable taskRunnable = () -> { value.set(value.get() + 1); System.out.println("\tExecuted task."); }; - Task task = new Task(taskRunnable, schedule); - scheduler.addTask(task); + scheduler.addTask(Task.of(taskRunnable, schedule)); assertEquals(0, value.get()); scheduler.start(); System.out.println("Waiting 3.5 seconds for 3 iterations.");