From 3415b04c7326e1d78dae92b8997fb9355aff6223 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 14 Jun 2023 12:57:17 +0200 Subject: [PATCH] Use nanosecond precision for scheduling (aligned with calculations) Closes gh-30666 --- .../scheduling/TriggerContext.java | 5 ++- .../concurrent/ConcurrentTaskScheduler.java | 10 +++--- .../concurrent/ReschedulingRunnable.java | 8 ++--- .../concurrent/ThreadPoolTaskScheduler.java | 10 +++--- .../config/ScheduledTaskRegistrar.java | 2 +- .../scheduling/support/CronExpression.java | 34 ++++++------------- .../scheduling/support/CronTrigger.java | 7 ++-- 7 files changed, 33 insertions(+), 43 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/scheduling/TriggerContext.java b/spring-context/src/main/java/org/springframework/scheduling/TriggerContext.java index cb8ccc2f399..d70159b14f0 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/TriggerContext.java +++ b/spring-context/src/main/java/org/springframework/scheduling/TriggerContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import org.springframework.lang.Nullable; * of a given task. * * @author Juergen Hoeller + * @author Arjen Poutsma * @since 3.0 */ public interface TriggerContext { @@ -78,6 +79,7 @@ public interface TriggerContext { /** * Return the last actual execution time of the task, * or {@code null} if not scheduled before. + * @since 6.0 */ @Nullable Instant lastActualExecution(); @@ -98,6 +100,7 @@ public interface TriggerContext { /** * Return the last completion time of the task, * or {@code null} if not scheduled before. + * @since 6.0 */ @Nullable Instant lastCompletion(); diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskScheduler.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskScheduler.java index 32e4592b99c..3e580dedeb2 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskScheduler.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskScheduler.java @@ -211,7 +211,7 @@ public class ConcurrentTaskScheduler extends ConcurrentTaskExecutor implements T public ScheduledFuture schedule(Runnable task, Instant startTime) { Duration initialDelay = Duration.between(this.clock.instant(), startTime); try { - return this.scheduledExecutor.schedule(decorateTask(task, false), initialDelay.toMillis(), TimeUnit.MILLISECONDS); + return this.scheduledExecutor.schedule(decorateTask(task, false), initialDelay.toNanos(), TimeUnit.NANOSECONDS); } catch (RejectedExecutionException ex) { throw new TaskRejectedException("Executor [" + this.scheduledExecutor + "] did not accept task: " + task, ex); @@ -222,7 +222,7 @@ public class ConcurrentTaskScheduler extends ConcurrentTaskExecutor implements T public ScheduledFuture scheduleAtFixedRate(Runnable task, Instant startTime, Duration period) { Duration initialDelay = Duration.between(this.clock.instant(), startTime); try { - return this.scheduledExecutor.scheduleAtFixedRate(decorateTask(task, true), initialDelay.toMillis(), period.toMillis(), TimeUnit.MILLISECONDS); + return this.scheduledExecutor.scheduleAtFixedRate(decorateTask(task, true), initialDelay.toNanos(), period.toNanos(), TimeUnit.NANOSECONDS); } catch (RejectedExecutionException ex) { throw new TaskRejectedException("Executor [" + this.scheduledExecutor + "] did not accept task: " + task, ex); @@ -232,7 +232,7 @@ public class ConcurrentTaskScheduler extends ConcurrentTaskExecutor implements T @Override public ScheduledFuture scheduleAtFixedRate(Runnable task, Duration period) { try { - return this.scheduledExecutor.scheduleAtFixedRate(decorateTask(task, true), 0, period.toMillis(), TimeUnit.MILLISECONDS); + return this.scheduledExecutor.scheduleAtFixedRate(decorateTask(task, true), 0, period.toNanos(), TimeUnit.NANOSECONDS); } catch (RejectedExecutionException ex) { throw new TaskRejectedException("Executor [" + this.scheduledExecutor + "] did not accept task: " + task, ex); @@ -243,7 +243,7 @@ public class ConcurrentTaskScheduler extends ConcurrentTaskExecutor implements T public ScheduledFuture scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay) { Duration initialDelay = Duration.between(this.clock.instant(), startTime); try { - return this.scheduledExecutor.scheduleWithFixedDelay(decorateTask(task, true), initialDelay.toMillis(), delay.toMillis(), TimeUnit.MILLISECONDS); + return this.scheduledExecutor.scheduleWithFixedDelay(decorateTask(task, true), initialDelay.toNanos(), delay.toNanos(), TimeUnit.NANOSECONDS); } catch (RejectedExecutionException ex) { throw new TaskRejectedException("Executor [" + this.scheduledExecutor + "] did not accept task: " + task, ex); @@ -253,7 +253,7 @@ public class ConcurrentTaskScheduler extends ConcurrentTaskExecutor implements T @Override public ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay) { try { - return this.scheduledExecutor.scheduleWithFixedDelay(decorateTask(task, true), 0, delay.toMillis(), TimeUnit.MILLISECONDS); + return this.scheduledExecutor.scheduleWithFixedDelay(decorateTask(task, true), 0, delay.toNanos(), TimeUnit.NANOSECONDS); } catch (RejectedExecutionException ex) { throw new TaskRejectedException("Executor [" + this.scheduledExecutor + "] did not accept task: " + task, ex); diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ReschedulingRunnable.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ReschedulingRunnable.java index bb22d261d02..10fbaaab236 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ReschedulingRunnable.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ReschedulingRunnable.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -80,7 +80,7 @@ class ReschedulingRunnable extends DelegatingErrorHandlingRunnable implements Sc return null; } Duration initialDelay = Duration.between(this.triggerContext.getClock().instant(), this.scheduledExecutionTime); - this.currentFuture = this.executor.schedule(this, initialDelay.toMillis(), TimeUnit.MILLISECONDS); + this.currentFuture = this.executor.schedule(this, initialDelay.toNanos(), TimeUnit.NANOSECONDS); return this; } } @@ -158,8 +158,8 @@ class ReschedulingRunnable extends DelegatingErrorHandlingRunnable implements Sc if (this == other) { return 0; } - long diff = getDelay(TimeUnit.MILLISECONDS) - other.getDelay(TimeUnit.MILLISECONDS); - return (diff == 0 ? 0 : ((diff < 0)? -1 : 1)); + long diff = getDelay(TimeUnit.NANOSECONDS) - other.getDelay(TimeUnit.NANOSECONDS); + return (diff == 0 ? 0 : (diff < 0 ? -1 : 1)); } } diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java index 42f0e5bcfab..07a8b9855a7 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java @@ -382,7 +382,7 @@ public class ThreadPoolTaskScheduler extends ExecutorConfigurationSupport ScheduledExecutorService executor = getScheduledExecutor(); Duration initialDelay = Duration.between(this.clock.instant(), startTime); try { - return executor.schedule(errorHandlingTask(task, false), initialDelay.toMillis(), TimeUnit.MILLISECONDS); + return executor.schedule(errorHandlingTask(task, false), initialDelay.toNanos(), TimeUnit.NANOSECONDS); } catch (RejectedExecutionException ex) { throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); @@ -394,7 +394,7 @@ public class ThreadPoolTaskScheduler extends ExecutorConfigurationSupport ScheduledExecutorService executor = getScheduledExecutor(); Duration initialDelay = Duration.between(this.clock.instant(), startTime); try { - return executor.scheduleAtFixedRate(errorHandlingTask(task, true), initialDelay.toMillis(), period.toMillis(), TimeUnit.MILLISECONDS); + return executor.scheduleAtFixedRate(errorHandlingTask(task, true), initialDelay.toNanos(), period.toNanos(), TimeUnit.NANOSECONDS); } catch (RejectedExecutionException ex) { throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); @@ -405,7 +405,7 @@ public class ThreadPoolTaskScheduler extends ExecutorConfigurationSupport public ScheduledFuture scheduleAtFixedRate(Runnable task, Duration period) { ScheduledExecutorService executor = getScheduledExecutor(); try { - return executor.scheduleAtFixedRate(errorHandlingTask(task, true), 0, period.toMillis(), TimeUnit.MILLISECONDS); + return executor.scheduleAtFixedRate(errorHandlingTask(task, true), 0, period.toNanos(), TimeUnit.NANOSECONDS); } catch (RejectedExecutionException ex) { throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); @@ -417,7 +417,7 @@ public class ThreadPoolTaskScheduler extends ExecutorConfigurationSupport ScheduledExecutorService executor = getScheduledExecutor(); Duration initialDelay = Duration.between(this.clock.instant(), startTime); try { - return executor.scheduleWithFixedDelay(errorHandlingTask(task, true), initialDelay.toMillis(), delay.toMillis(), TimeUnit.MILLISECONDS); + return executor.scheduleWithFixedDelay(errorHandlingTask(task, true), initialDelay.toNanos(), delay.toNanos(), TimeUnit.NANOSECONDS); } catch (RejectedExecutionException ex) { throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); @@ -428,7 +428,7 @@ public class ThreadPoolTaskScheduler extends ExecutorConfigurationSupport public ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay) { ScheduledExecutorService executor = getScheduledExecutor(); try { - return executor.scheduleWithFixedDelay(errorHandlingTask(task, true), 0, delay.toMillis(), TimeUnit.MILLISECONDS); + return executor.scheduleWithFixedDelay(errorHandlingTask(task, true), 0, delay.toNanos(), TimeUnit.NANOSECONDS); } catch (RejectedExecutionException ex) { throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java b/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java index 0ea0d2fe01a..5ded5609613 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java +++ b/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java @@ -485,7 +485,7 @@ public class ScheduledTaskRegistrar implements ScheduledTaskHolder, Initializing } if (this.taskScheduler != null) { Duration initialDelay = task.getInitialDelayDuration(); - if (initialDelay.toMillis() > 0) { + if (initialDelay.toNanos() > 0) { Instant startTime = this.taskScheduler.getClock().instant().plus(initialDelay); scheduledTask.future = this.taskScheduler.scheduleAtFixedRate(task.getRunnable(), startTime, task.getIntervalDuration()); diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java index bd20711dc96..0e6008de625 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java @@ -57,18 +57,12 @@ public final class CronExpression { private final String expression; - private CronExpression( - CronField seconds, - CronField minutes, - CronField hours, - CronField daysOfMonth, - CronField months, - CronField daysOfWeek, - String expression) { + private CronExpression(CronField seconds, CronField minutes, CronField hours, + CronField daysOfMonth, CronField months, CronField daysOfWeek, String expression) { - // reverse order, to make big changes first - // to make sure we end up at 0 nanos, we add an extra field - this.fields = new CronField[]{daysOfWeek, months, daysOfMonth, hours, minutes, seconds, CronField.zeroNanos()}; + // Reverse order, to make big changes first. + // To make sure we end up at 0 nanos, we add an extra field. + this.fields = new CronField[] {daysOfWeek, months, daysOfMonth, hours, minutes, seconds, CronField.zeroNanos()}; this.expression = expression; } @@ -268,26 +262,18 @@ public final class CronExpression { @Override - public int hashCode() { - return Arrays.hashCode(this.fields); + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof CronExpression that && + Arrays.equals(this.fields, that.fields))); } @Override - public boolean equals(@Nullable Object o) { - if (this == o) { - return true; - } - if (o instanceof CronExpression other) { - return Arrays.equals(this.fields, other.fields); - } - else { - return false; - } + public int hashCode() { + return Arrays.hashCode(this.fields); } /** * Return the expression string used to create this {@code CronExpression}. - * @return the expression string */ @Override public String toString() { diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java index aa4bd38afac..7bcb8ed2fa7 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java @@ -89,7 +89,7 @@ public class CronTrigger implements Trigger { /** * Determine the next execution time according to the given trigger context. *

Next execution times are calculated based on the - * {@linkplain TriggerContext#lastCompletionTime completion time} of the + * {@linkplain TriggerContext#lastCompletion completion time} of the * previous execution; therefore, overlapping executions won't occur. */ @Override @@ -114,8 +114,9 @@ public class CronTrigger implements Trigger { @Override - public boolean equals(@Nullable Object obj) { - return (this == obj || (obj instanceof CronTrigger that && this.expression.equals(that.expression))); + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof CronTrigger that && + this.expression.equals(that.expression))); } @Override