From dc2c8d6094ba662eeb1b9f4bad34be52703312c4 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 11 Jun 2024 19:29:21 +0200 Subject: [PATCH] Add execution metadata to tasks and scheduled tasks This commit adds new information about the execution and scheduling of tasks. The `Task` type now exposes the `TaskExecutionOutcome` of the latest execution; this includes the instant the execution started, the execution outcome and any thrown exception. The `ScheduledTask` contract can now provide the time when the next execution is scheduled. Closes gh-24560 --- .../scheduling/config/ScheduledTask.java | 21 ++- .../scheduling/config/Task.java | 45 +++++- .../config/TaskExecutionOutcome.java | 74 ++++++++++ ...duledAnnotationBeanPostProcessorTests.java | 137 ++++++------------ .../scheduling/config/ScheduledTaskTests.java | 108 ++++++++++++++ ...heduledTasksBeanDefinitionParserTests.java | 15 +- .../config/TaskExecutionOutcomeTests.java | 98 +++++++++++++ .../scheduling/config/TaskTests.java | 96 ++++++++++++ 8 files changed, 494 insertions(+), 100 deletions(-) create mode 100644 spring-context/src/main/java/org/springframework/scheduling/config/TaskExecutionOutcome.java create mode 100644 spring-context/src/test/java/org/springframework/scheduling/config/ScheduledTaskTests.java create mode 100644 spring-context/src/test/java/org/springframework/scheduling/config/TaskExecutionOutcomeTests.java create mode 100644 spring-context/src/test/java/org/springframework/scheduling/config/TaskTests.java diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTask.java b/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTask.java index 799f87464b4a..9b6e7f7c0172 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTask.java +++ b/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTask.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -16,7 +16,9 @@ package org.springframework.scheduling.config; +import java.time.Instant; import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; import org.springframework.lang.Nullable; @@ -25,6 +27,7 @@ * used as a return value for scheduling methods. * * @author Juergen Hoeller + * @author Brian Clozel * @since 4.3 * @see ScheduledTaskRegistrar#scheduleCronTask(CronTask) * @see ScheduledTaskRegistrar#scheduleFixedRateTask(FixedRateTask) @@ -76,6 +79,22 @@ public void cancel(boolean mayInterruptIfRunning) { } } + /** + * Return the next scheduled execution of the task, or {@code null} + * if the task has been cancelled or no new execution is scheduled. + * @since 6.2 + */ + @Nullable + public Instant nextExecution() { + if (this.future != null && !this.future.isCancelled()) { + long delay = this.future.getDelay(TimeUnit.MILLISECONDS); + if (delay > 0) { + return Instant.now().plusMillis(delay); + } + } + return null; + } + @Override public String toString() { return this.task.toString(); diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/Task.java b/spring-context/src/main/java/org/springframework/scheduling/config/Task.java index 3d9a6e79a775..d1a3373fa8d8 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/config/Task.java +++ b/spring-context/src/main/java/org/springframework/scheduling/config/Task.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2024 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. @@ -16,6 +16,8 @@ package org.springframework.scheduling.config; +import java.time.Instant; + import org.springframework.util.Assert; /** @@ -24,12 +26,15 @@ * * @author Chris Beams * @author Juergen Hoeller + * @author Brian Clozel * @since 3.2 */ public class Task { private final Runnable runnable; + private TaskExecutionOutcome lastExecutionOutcome; + /** * Create a new {@code Task}. @@ -37,7 +42,8 @@ public class Task { */ public Task(Runnable runnable) { Assert.notNull(runnable, "Runnable must not be null"); - this.runnable = runnable; + this.runnable = new OutcomeTrackingRunnable(runnable); + this.lastExecutionOutcome = TaskExecutionOutcome.create(); } @@ -48,10 +54,45 @@ public Runnable getRunnable() { return this.runnable; } + /** + * Return the outcome of the last task execution. + * @since 6.2 + */ + public TaskExecutionOutcome getLastExecutionOutcome() { + return this.lastExecutionOutcome; + } @Override public String toString() { return this.runnable.toString(); } + + private class OutcomeTrackingRunnable implements Runnable { + + private final Runnable runnable; + + public OutcomeTrackingRunnable(Runnable runnable) { + this.runnable = runnable; + } + + @Override + public void run() { + try { + Task.this.lastExecutionOutcome = Task.this.lastExecutionOutcome.start(Instant.now()); + this.runnable.run(); + Task.this.lastExecutionOutcome = Task.this.lastExecutionOutcome.success(); + } + catch (Throwable exc) { + Task.this.lastExecutionOutcome = Task.this.lastExecutionOutcome.failure(exc); + throw exc; + } + } + + @Override + public String toString() { + return this.runnable.toString(); + } + } + } diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/TaskExecutionOutcome.java b/spring-context/src/main/java/org/springframework/scheduling/config/TaskExecutionOutcome.java new file mode 100644 index 000000000000..d4656c037cda --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/config/TaskExecutionOutcome.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.scheduling.config; + +import java.time.Instant; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Outcome of a {@link Task} execution. + * @param executionTime the instant when the task execution started, {@code null} if the task has not started. + * @param status the {@link Status} of the execution outcome. + * @param throwable the exception thrown from the task execution, if any. + * @author Brian Clozel + * @since 6.2 + */ +public record TaskExecutionOutcome(@Nullable Instant executionTime, Status status, @Nullable Throwable throwable) { + + TaskExecutionOutcome start(Instant executionTime) { + return new TaskExecutionOutcome(executionTime, Status.STARTED, null); + } + + TaskExecutionOutcome success() { + Assert.state(this.executionTime != null, "Task has not been started yet"); + return new TaskExecutionOutcome(this.executionTime, Status.SUCCESS, null); + } + + TaskExecutionOutcome failure(Throwable throwable) { + Assert.state(this.executionTime != null, "Task has not been started yet"); + return new TaskExecutionOutcome(this.executionTime, Status.ERROR, throwable); + } + + static TaskExecutionOutcome create() { + return new TaskExecutionOutcome(null, Status.NONE, null); + } + + + /** + * Status of the task execution outcome. + */ + public enum Status { + /** + * The task has not been executed so far. + */ + NONE, + /** + * The task execution has been started and is ongoing. + */ + STARTED, + /** + * The task execution finished successfully. + */ + SUCCESS, + /** + * The task execution finished with an error. + */ + ERROR + } +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessorTests.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessorTests.java index 07ee096636a9..b3cf04cc2698 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessorTests.java @@ -21,7 +21,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.lang.reflect.Method; import java.time.Duration; import java.time.Instant; import java.time.ZoneId; @@ -32,6 +31,7 @@ import java.util.Properties; import java.util.concurrent.TimeUnit; +import org.assertj.core.api.AbstractAssert; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ParameterContext; @@ -116,11 +116,7 @@ void fixedDelayTask(@NameToClass Class beanClass, long expectedInterval) { new DirectFieldAccessor(registrar).getPropertyValue("fixedDelayTasks"); assertThat(fixedDelayTasks).hasSize(1); IntervalTask task = fixedDelayTasks.get(0); - ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); - Object targetObject = runnable.getTarget(); - Method targetMethod = runnable.getMethod(); - assertThat(targetObject).isEqualTo(target); - assertThat(targetMethod.getName()).isEqualTo("fixedDelay"); + assertThatScheduledRunnable(task.getRunnable()).hasTarget(target).hasMethodName("fixedDelay"); assertThat(task.getInitialDelayDuration()).isZero(); assertThat(task.getIntervalDuration()).isEqualTo( Duration.ofMillis(expectedInterval < 0 ? Long.MAX_VALUE : expectedInterval)); @@ -150,11 +146,7 @@ void fixedRateTask(@NameToClass Class beanClass, long expectedInterval) { new DirectFieldAccessor(registrar).getPropertyValue("fixedRateTasks"); assertThat(fixedRateTasks).hasSize(1); IntervalTask task = fixedRateTasks.get(0); - ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); - Object targetObject = runnable.getTarget(); - Method targetMethod = runnable.getMethod(); - assertThat(targetObject).isEqualTo(target); - assertThat(targetMethod.getName()).isEqualTo("fixedRate"); + assertThatScheduledRunnable(task.getRunnable()).hasTarget(target).hasMethodName("fixedRate"); assertSoftly(softly -> { softly.assertThat(task.getInitialDelayDuration()).as("initial delay").isZero(); softly.assertThat(task.getIntervalDuration()).as("interval").isEqualTo(Duration.ofMillis(expectedInterval)); @@ -185,11 +177,7 @@ void fixedRateTaskWithInitialDelay(@NameToClass Class beanClass, long expecte new DirectFieldAccessor(registrar).getPropertyValue("fixedRateTasks"); assertThat(fixedRateTasks).hasSize(1); IntervalTask task = fixedRateTasks.get(0); - ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); - Object targetObject = runnable.getTarget(); - Method targetMethod = runnable.getMethod(); - assertThat(targetObject).isEqualTo(target); - assertThat(targetMethod.getName()).isEqualTo("fixedRate"); + assertThatScheduledRunnable(task.getRunnable()).hasTarget(target).hasMethodName("fixedRate"); assertSoftly(softly -> { softly.assertThat(task.getInitialDelayDuration()).as("initial delay").isEqualTo(Duration.ofMillis(expectedInitialDelay)); softly.assertThat(task.getIntervalDuration()).as("interval").isEqualTo(Duration.ofMillis(expectedInterval)); @@ -250,19 +238,11 @@ private void severalFixedRates(StaticApplicationContext context, new DirectFieldAccessor(registrar).getPropertyValue("fixedRateTasks"); assertThat(fixedRateTasks).hasSize(2); IntervalTask task1 = fixedRateTasks.get(0); - ScheduledMethodRunnable runnable1 = (ScheduledMethodRunnable) task1.getRunnable(); - Object targetObject = runnable1.getTarget(); - Method targetMethod = runnable1.getMethod(); - assertThat(targetObject).isEqualTo(target); - assertThat(targetMethod.getName()).isEqualTo("fixedRate"); + assertThatScheduledRunnable(task1.getRunnable()).hasTarget(target).hasMethodName("fixedRate"); assertThat(task1.getInitialDelayDuration()).isZero(); assertThat(task1.getIntervalDuration()).isEqualTo(Duration.ofMillis(4_000L)); IntervalTask task2 = fixedRateTasks.get(1); - ScheduledMethodRunnable runnable2 = (ScheduledMethodRunnable) task2.getRunnable(); - targetObject = runnable2.getTarget(); - targetMethod = runnable2.getMethod(); - assertThat(targetObject).isEqualTo(target); - assertThat(targetMethod.getName()).isEqualTo("fixedRate"); + assertThatScheduledRunnable(task2.getRunnable()).hasTarget(target).hasMethodName("fixedRate"); assertThat(task2.getInitialDelayDuration()).isEqualTo(Duration.ofMillis(2_000L)); assertThat(task2.getIntervalDuration()).isEqualTo(Duration.ofMillis(4_000L)); } @@ -286,11 +266,7 @@ void oneTimeTask() { new DirectFieldAccessor(registrar).getPropertyValue("oneTimeTasks"); assertThat(oneTimeTasks).hasSize(1); OneTimeTask task = oneTimeTasks.get(0); - ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); - Object targetObject = runnable.getTarget(); - Method targetMethod = runnable.getMethod(); - assertThat(targetObject).isEqualTo(target); - assertThat(targetMethod.getName()).isEqualTo("oneTimeTask"); + assertThatScheduledRunnable(task.getRunnable()).hasTarget(target).hasMethodName("oneTimeTask"); assertThat(task.getInitialDelayDuration()).isEqualTo(Duration.ofMillis(2_000L)); } @@ -313,11 +289,7 @@ void cronTask() { new DirectFieldAccessor(registrar).getPropertyValue("cronTasks"); assertThat(cronTasks).hasSize(1); CronTask task = cronTasks.get(0); - ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); - Object targetObject = runnable.getTarget(); - Method targetMethod = runnable.getMethod(); - assertThat(targetObject).isEqualTo(target); - assertThat(targetMethod.getName()).isEqualTo("cron"); + assertThatScheduledRunnable(task.getRunnable()).hasTarget(target).hasMethodName("cron"); assertThat(task.getExpression()).isEqualTo("*/7 * * * * ?"); } @@ -340,11 +312,7 @@ void cronTaskWithZone() { new DirectFieldAccessor(registrar).getPropertyValue("cronTasks"); assertThat(cronTasks).hasSize(1); CronTask task = cronTasks.get(0); - ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); - Object targetObject = runnable.getTarget(); - Method targetMethod = runnable.getMethod(); - assertThat(targetObject).isEqualTo(target); - assertThat(targetMethod.getName()).isEqualTo("cron"); + assertThatScheduledRunnable(task.getRunnable()).hasTarget(target).hasMethodName("cron"); assertThat(task.getExpression()).isEqualTo("0 0 0-4,6-23 * * ?"); Trigger trigger = task.getTrigger(); assertThat(trigger).isNotNull(); @@ -404,11 +372,9 @@ void cronTaskWithScopedProxy() { new DirectFieldAccessor(registrar).getPropertyValue("cronTasks"); assertThat(cronTasks).hasSize(1); CronTask task = cronTasks.get(0); - ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); - Object targetObject = runnable.getTarget(); - Method targetMethod = runnable.getMethod(); - assertThat(targetObject).isEqualTo(context.getBean(ScopedProxyUtils.getTargetBeanName("target"))); - assertThat(targetMethod.getName()).isEqualTo("cron"); + assertThatScheduledRunnable(task.getRunnable()) + .hasTarget(context.getBean(ScopedProxyUtils.getTargetBeanName("target"))) + .hasMethodName("cron"); assertThat(task.getExpression()).isEqualTo("*/7 * * * * ?"); } @@ -431,11 +397,7 @@ void metaAnnotationWithFixedRate() { new DirectFieldAccessor(registrar).getPropertyValue("fixedRateTasks"); assertThat(fixedRateTasks).hasSize(1); IntervalTask task = fixedRateTasks.get(0); - ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); - Object targetObject = runnable.getTarget(); - Method targetMethod = runnable.getMethod(); - assertThat(targetObject).isEqualTo(target); - assertThat(targetMethod.getName()).isEqualTo("checkForUpdates"); + assertThatScheduledRunnable(task.getRunnable()).hasTarget(target).hasMethodName("checkForUpdates"); assertThat(task.getIntervalDuration()).isEqualTo(Duration.ofMillis(5_000L)); } @@ -458,11 +420,7 @@ void composedAnnotationWithInitialDelayAndFixedRate() { new DirectFieldAccessor(registrar).getPropertyValue("fixedRateTasks"); assertThat(fixedRateTasks).hasSize(1); IntervalTask task = fixedRateTasks.get(0); - ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); - Object targetObject = runnable.getTarget(); - Method targetMethod = runnable.getMethod(); - assertThat(targetObject).isEqualTo(target); - assertThat(targetMethod.getName()).isEqualTo("checkForUpdates"); + assertThatScheduledRunnable(task.getRunnable()).hasTarget(target).hasMethodName("checkForUpdates"); assertThat(task.getIntervalDuration()).isEqualTo(Duration.ofMillis(5_000L)); assertThat(task.getInitialDelayDuration()).isEqualTo(Duration.ofMillis(1_000L)); } @@ -486,11 +444,7 @@ void metaAnnotationWithCronExpression() { new DirectFieldAccessor(registrar).getPropertyValue("cronTasks"); assertThat(cronTasks).hasSize(1); CronTask task = cronTasks.get(0); - ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); - Object targetObject = runnable.getTarget(); - Method targetMethod = runnable.getMethod(); - assertThat(targetObject).isEqualTo(target); - assertThat(targetMethod.getName()).isEqualTo("generateReport"); + assertThatScheduledRunnable(task.getRunnable()).hasTarget(target).hasMethodName("generateReport"); assertThat(task.getExpression()).isEqualTo("0 0 * * * ?"); } @@ -519,11 +473,7 @@ void propertyPlaceholderWithCron() { new DirectFieldAccessor(registrar).getPropertyValue("cronTasks"); assertThat(cronTasks).hasSize(1); CronTask task = cronTasks.get(0); - ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); - Object targetObject = runnable.getTarget(); - Method targetMethod = runnable.getMethod(); - assertThat(targetObject).isEqualTo(target); - assertThat(targetMethod.getName()).isEqualTo("x"); + assertThatScheduledRunnable(task.getRunnable()).hasTarget(target).hasMethodName("x"); assertThat(task.getExpression()).isEqualTo(businessHoursCronExpression); } @@ -578,11 +528,7 @@ void propertyPlaceholderWithFixedDelay(@NameToClass Class beanClass, String f new DirectFieldAccessor(registrar).getPropertyValue("fixedDelayTasks"); assertThat(fixedDelayTasks).hasSize(1); IntervalTask task = fixedDelayTasks.get(0); - ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); - Object targetObject = runnable.getTarget(); - Method targetMethod = runnable.getMethod(); - assertThat(targetObject).isEqualTo(target); - assertThat(targetMethod.getName()).isEqualTo("fixedDelay"); + assertThatScheduledRunnable(task.getRunnable()).hasTarget(target).hasMethodName("fixedDelay"); assertSoftly(softly -> { softly.assertThat(task.getInitialDelayDuration()).as("initial delay").isEqualTo(Duration.ofMillis(expectedInitialDelay)); softly.assertThat(task.getIntervalDuration()).as("interval").isEqualTo(Duration.ofMillis(expectedInterval)); @@ -622,11 +568,7 @@ void propertyPlaceholderWithFixedRate(@NameToClass Class beanClass, String fi new DirectFieldAccessor(registrar).getPropertyValue("fixedRateTasks"); assertThat(fixedRateTasks).hasSize(1); IntervalTask task = fixedRateTasks.get(0); - ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); - Object targetObject = runnable.getTarget(); - Method targetMethod = runnable.getMethod(); - assertThat(targetObject).isEqualTo(target); - assertThat(targetMethod.getName()).isEqualTo("fixedRate"); + assertThatScheduledRunnable(task.getRunnable()).hasTarget(target).hasMethodName("fixedRate"); assertSoftly(softly -> { softly.assertThat(task.getInitialDelayDuration()).as("initial delay").isEqualTo(Duration.ofMillis(expectedInitialDelay)); softly.assertThat(task.getIntervalDuration()).as("interval").isEqualTo(Duration.ofMillis(expectedInterval)); @@ -656,11 +598,7 @@ void expressionWithCron() { new DirectFieldAccessor(registrar).getPropertyValue("cronTasks"); assertThat(cronTasks).hasSize(1); CronTask task = cronTasks.get(0); - ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); - Object targetObject = runnable.getTarget(); - Method targetMethod = runnable.getMethod(); - assertThat(targetObject).isEqualTo(target); - assertThat(targetMethod.getName()).isEqualTo("x"); + assertThatScheduledRunnable(task.getRunnable()).hasTarget(target).hasMethodName("x"); assertThat(task.getExpression()).isEqualTo(businessHoursCronExpression); } @@ -689,11 +627,7 @@ void propertyPlaceholderForMetaAnnotation() { new DirectFieldAccessor(registrar).getPropertyValue("cronTasks"); assertThat(cronTasks).hasSize(1); CronTask task = cronTasks.get(0); - ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); - Object targetObject = runnable.getTarget(); - Method targetMethod = runnable.getMethod(); - assertThat(targetObject).isEqualTo(target); - assertThat(targetMethod.getName()).isEqualTo("y"); + assertThatScheduledRunnable(task.getRunnable()).hasTarget(target).hasMethodName("y"); assertThat(task.getExpression()).isEqualTo(businessHoursCronExpression); } @@ -716,11 +650,7 @@ void nonVoidReturnType() { new DirectFieldAccessor(registrar).getPropertyValue("cronTasks"); assertThat(cronTasks).hasSize(1); CronTask task = cronTasks.get(0); - ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); - Object targetObject = runnable.getTarget(); - Method targetMethod = runnable.getMethod(); - assertThat(targetObject).isEqualTo(target); - assertThat(targetMethod.getName()).isEqualTo("cron"); + assertThatScheduledRunnable(task.getRunnable()).hasTarget(target).hasMethodName("cron"); assertThat(task.getExpression()).isEqualTo("0 0 9-17 * * MON-FRI"); } @@ -1088,4 +1018,29 @@ public Class convert(Object beanClassName, ParameterContext context) throws A } } + static ScheduledMethodRunnableAssert assertThatScheduledRunnable(Runnable runnable) { + return new ScheduledMethodRunnableAssert(runnable); + } + + static class ScheduledMethodRunnableAssert extends AbstractAssert { + + public ScheduledMethodRunnableAssert(Runnable actual) { + super(actual, ScheduledMethodRunnableAssert.class); + assertThat(actual).extracting("runnable").isInstanceOf(ScheduledMethodRunnable.class); + } + + public ScheduledMethodRunnableAssert hasTarget(Object target) { + isNotNull(); + assertThat(actual).extracting("runnable.target").isEqualTo(target); + return this; + } + + public ScheduledMethodRunnableAssert hasMethodName(String name) { + isNotNull(); + assertThat(actual).extracting("runnable.method.name").isEqualTo(name); + return this; + } + + } + } diff --git a/spring-context/src/test/java/org/springframework/scheduling/config/ScheduledTaskTests.java b/spring-context/src/test/java/org/springframework/scheduling/config/ScheduledTaskTests.java new file mode 100644 index 000000000000..b7cf63cb65da --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/config/ScheduledTaskTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.scheduling.config; + + +import java.time.Duration; +import java.time.Instant; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ScheduledTask}. + * @author Brian Clozel + */ +class ScheduledTaskTests { + + private CountingRunnable countingRunnable = new CountingRunnable(); + + private SimpleAsyncTaskScheduler taskScheduler = new SimpleAsyncTaskScheduler(); + + private ScheduledTaskRegistrar taskRegistrar = new ScheduledTaskRegistrar(); + + @BeforeEach + void setup() { + this.taskRegistrar.setTaskScheduler(this.taskScheduler); + taskScheduler.start(); + } + + @AfterEach + void tearDown() { + taskScheduler.stop(); + } + + @Test + void shouldReturnConfiguredTask() { + Task task = new Task(countingRunnable); + ScheduledTask scheduledTask = new ScheduledTask(task); + assertThat(scheduledTask.getTask()).isEqualTo(task); + } + + @Test + void shouldUseTaskToString() { + Task task = new Task(countingRunnable); + ScheduledTask scheduledTask = new ScheduledTask(task); + assertThat(scheduledTask.toString()).isEqualTo(task.toString()); + } + + @Test + void unscheduledTaskShouldNotHaveNextExecution() { + ScheduledTask scheduledTask = new ScheduledTask(new Task(countingRunnable)); + assertThat(scheduledTask.nextExecution()).isNull(); + assertThat(countingRunnable.executionCount).isZero(); + } + + @Test + void scheduledTaskShouldHaveNextExecution() { + ScheduledTask scheduledTask = taskRegistrar.scheduleFixedDelayTask(new FixedDelayTask(countingRunnable, + Duration.ofSeconds(10), Duration.ofSeconds(10))); + assertThat(scheduledTask.nextExecution()).isBefore(Instant.now().plusSeconds(11)); + } + + @Test + void cancelledTaskShouldNotHaveNextExecution() { + ScheduledTask scheduledTask = taskRegistrar.scheduleFixedDelayTask(new FixedDelayTask(countingRunnable, + Duration.ofSeconds(10), Duration.ofSeconds(10))); + scheduledTask.cancel(true); + assertThat(scheduledTask.nextExecution()).isNull(); + } + + @Test + void singleExecutionShouldNotHaveNextExecution() { + ScheduledTask scheduledTask = taskRegistrar.scheduleOneTimeTask(new OneTimeTask(countingRunnable, Duration.ofSeconds(0))); + Awaitility.await().atMost(Duration.ofSeconds(5)).until(() -> countingRunnable.executionCount > 0); + assertThat(scheduledTask.nextExecution()).isNull(); + } + + class CountingRunnable implements Runnable { + + int executionCount; + + @Override + public void run() { + executionCount++; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/config/ScheduledTasksBeanDefinitionParserTests.java b/spring-context/src/test/java/org/springframework/scheduling/config/ScheduledTasksBeanDefinitionParserTests.java index c6ab6d340f9f..a58a0f9d82d3 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/config/ScheduledTasksBeanDefinitionParserTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/config/ScheduledTasksBeanDefinitionParserTests.java @@ -16,11 +16,11 @@ package org.springframework.scheduling.config; -import java.lang.reflect.Method; import java.time.Duration; import java.time.Instant; import java.util.List; +import org.assertj.core.api.ObjectAssert; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -32,6 +32,7 @@ import org.springframework.scheduling.support.ScheduledMethodRunnable; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.InstanceOfAssertFactories.type; /** * @author Mark Fisher @@ -68,11 +69,13 @@ void checkTarget() { List tasks = (List) new DirectFieldAccessor( this.registrar).getPropertyValue("fixedRateTasks"); Runnable runnable = tasks.get(0).getRunnable(); - assertThat(runnable.getClass()).isEqualTo(ScheduledMethodRunnable.class); - Object targetObject = ((ScheduledMethodRunnable) runnable).getTarget(); - Method targetMethod = ((ScheduledMethodRunnable) runnable).getMethod(); - assertThat(targetObject).isEqualTo(this.testBean); - assertThat(targetMethod.getName()).isEqualTo("test"); + + ObjectAssert runnableAssert = assertThat(runnable) + .extracting("runnable") + .isInstanceOf(ScheduledMethodRunnable.class) + .asInstanceOf(type(ScheduledMethodRunnable.class)); + runnableAssert.extracting("target").isEqualTo(testBean); + runnableAssert.extracting("method.name").isEqualTo("test"); } @Test diff --git a/spring-context/src/test/java/org/springframework/scheduling/config/TaskExecutionOutcomeTests.java b/spring-context/src/test/java/org/springframework/scheduling/config/TaskExecutionOutcomeTests.java new file mode 100644 index 000000000000..5dfd5004f9db --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/config/TaskExecutionOutcomeTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.scheduling.config; + + +import java.time.Instant; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link TaskExecutionOutcome}. + * @author Brian Clozel + */ +class TaskExecutionOutcomeTests { + + @Test + void shouldCreateWithNoneStatus() { + TaskExecutionOutcome outcome = TaskExecutionOutcome.create(); + assertThat(outcome.status()).isEqualTo(TaskExecutionOutcome.Status.NONE); + assertThat(outcome.executionTime()).isNull(); + assertThat(outcome.throwable()).isNull(); + } + + @Test + void startedTaskShouldBeOngoing() { + TaskExecutionOutcome outcome = TaskExecutionOutcome.create(); + Instant now = Instant.now(); + outcome = outcome.start(now); + assertThat(outcome.status()).isEqualTo(TaskExecutionOutcome.Status.STARTED); + assertThat(outcome.executionTime()).isEqualTo(now); + assertThat(outcome.throwable()).isNull(); + } + + @Test + void shouldRejectSuccessWhenNotStarted() { + TaskExecutionOutcome outcome = TaskExecutionOutcome.create(); + assertThatIllegalStateException().isThrownBy(outcome::success); + } + + @Test + void shouldRejectErrorWhenNotStarted() { + TaskExecutionOutcome outcome = TaskExecutionOutcome.create(); + assertThatIllegalStateException().isThrownBy(() -> outcome.failure(new IllegalArgumentException("test error"))); + } + + @Test + void finishedTaskShouldBeSuccessful() { + TaskExecutionOutcome outcome = TaskExecutionOutcome.create(); + Instant now = Instant.now(); + outcome = outcome.start(now); + outcome = outcome.success(); + assertThat(outcome.status()).isEqualTo(TaskExecutionOutcome.Status.SUCCESS); + assertThat(outcome.executionTime()).isEqualTo(now); + assertThat(outcome.throwable()).isNull(); + } + + @Test + void errorTaskShouldBeFailure() { + TaskExecutionOutcome outcome = TaskExecutionOutcome.create(); + Instant now = Instant.now(); + outcome = outcome.start(now); + outcome = outcome.failure(new IllegalArgumentException(("test error"))); + assertThat(outcome.status()).isEqualTo(TaskExecutionOutcome.Status.ERROR); + assertThat(outcome.executionTime()).isEqualTo(now); + assertThat(outcome.throwable()).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void newTaskExecutionShouldNotFail() { + TaskExecutionOutcome outcome = TaskExecutionOutcome.create(); + Instant now = Instant.now(); + outcome = outcome.start(now); + outcome = outcome.failure(new IllegalArgumentException(("test error"))); + + outcome = outcome.start(now.plusSeconds(2)); + assertThat(outcome.status()).isEqualTo(TaskExecutionOutcome.Status.STARTED); + assertThat(outcome.executionTime()).isAfter(now); + assertThat(outcome.throwable()).isNull(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/config/TaskTests.java b/spring-context/src/test/java/org/springframework/scheduling/config/TaskTests.java new file mode 100644 index 000000000000..685024f71cb9 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/config/TaskTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.scheduling.config; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link Task}. + * @author Brian Clozel + */ +class TaskTests { + + @Test + void shouldRejectNullRunnable() { + assertThatIllegalArgumentException().isThrownBy(() -> new Task(null)); + } + + @Test + void initialStateShouldBeUnknown() { + TestRunnable testRunnable = new TestRunnable(); + Task task = new Task(testRunnable); + assertThat(testRunnable.hasRun).isFalse(); + TaskExecutionOutcome executionOutcome = task.getLastExecutionOutcome(); + assertThat(executionOutcome.executionTime()).isNull(); + assertThat(executionOutcome.status()).isEqualTo(TaskExecutionOutcome.Status.NONE); + assertThat(executionOutcome.throwable()).isNull(); + } + + @Test + void stateShouldUpdateAfterRun() { + TestRunnable testRunnable = new TestRunnable(); + Task task = new Task(testRunnable); + task.getRunnable().run(); + + assertThat(testRunnable.hasRun).isTrue(); + TaskExecutionOutcome executionOutcome = task.getLastExecutionOutcome(); + assertThat(executionOutcome.executionTime()).isInThePast(); + assertThat(executionOutcome.status()).isEqualTo(TaskExecutionOutcome.Status.SUCCESS); + assertThat(executionOutcome.throwable()).isNull(); + } + + @Test + void stateShouldUpdateAfterFailingRun() { + FailingTestRunnable testRunnable = new FailingTestRunnable(); + Task task = new Task(testRunnable); + assertThatIllegalStateException().isThrownBy(() -> task.getRunnable().run()); + + assertThat(testRunnable.hasRun).isTrue(); + TaskExecutionOutcome executionOutcome = task.getLastExecutionOutcome(); + assertThat(executionOutcome.executionTime()).isInThePast(); + assertThat(executionOutcome.status()).isEqualTo(TaskExecutionOutcome.Status.ERROR); + assertThat(executionOutcome.throwable()).isInstanceOf(IllegalStateException.class); + } + + + static class TestRunnable implements Runnable { + + boolean hasRun; + + @Override + public void run() { + this.hasRun = true; + } + } + + static class FailingTestRunnable implements Runnable { + + boolean hasRun; + + @Override + public void run() { + this.hasRun = true; + throw new IllegalStateException("test exception"); + } + } + + +}