Skip to content

Commit dc2c8d6

Browse files
committedJun 11, 2024·
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
1 parent 46dccd8 commit dc2c8d6

File tree

8 files changed

+494
-100
lines changed

8 files changed

+494
-100
lines changed
 

‎spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTask.java

+20-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,7 +16,9 @@
1616

1717
package org.springframework.scheduling.config;
1818

19+
import java.time.Instant;
1920
import java.util.concurrent.ScheduledFuture;
21+
import java.util.concurrent.TimeUnit;
2022

2123
import org.springframework.lang.Nullable;
2224

@@ -25,6 +27,7 @@
2527
* used as a return value for scheduling methods.
2628
*
2729
* @author Juergen Hoeller
30+
* @author Brian Clozel
2831
* @since 4.3
2932
* @see ScheduledTaskRegistrar#scheduleCronTask(CronTask)
3033
* @see ScheduledTaskRegistrar#scheduleFixedRateTask(FixedRateTask)
@@ -76,6 +79,22 @@ public void cancel(boolean mayInterruptIfRunning) {
7679
}
7780
}
7881

82+
/**
83+
* Return the next scheduled execution of the task, or {@code null}
84+
* if the task has been cancelled or no new execution is scheduled.
85+
* @since 6.2
86+
*/
87+
@Nullable
88+
public Instant nextExecution() {
89+
if (this.future != null && !this.future.isCancelled()) {
90+
long delay = this.future.getDelay(TimeUnit.MILLISECONDS);
91+
if (delay > 0) {
92+
return Instant.now().plusMillis(delay);
93+
}
94+
}
95+
return null;
96+
}
97+
7998
@Override
8099
public String toString() {
81100
return this.task.toString();

‎spring-context/src/main/java/org/springframework/scheduling/config/Task.java

+43-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.scheduling.config;
1818

19+
import java.time.Instant;
20+
1921
import org.springframework.util.Assert;
2022

2123
/**
@@ -24,20 +26,24 @@
2426
*
2527
* @author Chris Beams
2628
* @author Juergen Hoeller
29+
* @author Brian Clozel
2730
* @since 3.2
2831
*/
2932
public class Task {
3033

3134
private final Runnable runnable;
3235

36+
private TaskExecutionOutcome lastExecutionOutcome;
37+
3338

3439
/**
3540
* Create a new {@code Task}.
3641
* @param runnable the underlying task to execute
3742
*/
3843
public Task(Runnable runnable) {
3944
Assert.notNull(runnable, "Runnable must not be null");
40-
this.runnable = runnable;
45+
this.runnable = new OutcomeTrackingRunnable(runnable);
46+
this.lastExecutionOutcome = TaskExecutionOutcome.create();
4147
}
4248

4349

@@ -48,10 +54,45 @@ public Runnable getRunnable() {
4854
return this.runnable;
4955
}
5056

57+
/**
58+
* Return the outcome of the last task execution.
59+
* @since 6.2
60+
*/
61+
public TaskExecutionOutcome getLastExecutionOutcome() {
62+
return this.lastExecutionOutcome;
63+
}
5164

5265
@Override
5366
public String toString() {
5467
return this.runnable.toString();
5568
}
5669

70+
71+
private class OutcomeTrackingRunnable implements Runnable {
72+
73+
private final Runnable runnable;
74+
75+
public OutcomeTrackingRunnable(Runnable runnable) {
76+
this.runnable = runnable;
77+
}
78+
79+
@Override
80+
public void run() {
81+
try {
82+
Task.this.lastExecutionOutcome = Task.this.lastExecutionOutcome.start(Instant.now());
83+
this.runnable.run();
84+
Task.this.lastExecutionOutcome = Task.this.lastExecutionOutcome.success();
85+
}
86+
catch (Throwable exc) {
87+
Task.this.lastExecutionOutcome = Task.this.lastExecutionOutcome.failure(exc);
88+
throw exc;
89+
}
90+
}
91+
92+
@Override
93+
public String toString() {
94+
return this.runnable.toString();
95+
}
96+
}
97+
5798
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.scheduling.config;
18+
19+
import java.time.Instant;
20+
21+
import org.springframework.lang.Nullable;
22+
import org.springframework.util.Assert;
23+
24+
/**
25+
* Outcome of a {@link Task} execution.
26+
* @param executionTime the instant when the task execution started, {@code null} if the task has not started.
27+
* @param status the {@link Status} of the execution outcome.
28+
* @param throwable the exception thrown from the task execution, if any.
29+
* @author Brian Clozel
30+
* @since 6.2
31+
*/
32+
public record TaskExecutionOutcome(@Nullable Instant executionTime, Status status, @Nullable Throwable throwable) {
33+
34+
TaskExecutionOutcome start(Instant executionTime) {
35+
return new TaskExecutionOutcome(executionTime, Status.STARTED, null);
36+
}
37+
38+
TaskExecutionOutcome success() {
39+
Assert.state(this.executionTime != null, "Task has not been started yet");
40+
return new TaskExecutionOutcome(this.executionTime, Status.SUCCESS, null);
41+
}
42+
43+
TaskExecutionOutcome failure(Throwable throwable) {
44+
Assert.state(this.executionTime != null, "Task has not been started yet");
45+
return new TaskExecutionOutcome(this.executionTime, Status.ERROR, throwable);
46+
}
47+
48+
static TaskExecutionOutcome create() {
49+
return new TaskExecutionOutcome(null, Status.NONE, null);
50+
}
51+
52+
53+
/**
54+
* Status of the task execution outcome.
55+
*/
56+
public enum Status {
57+
/**
58+
* The task has not been executed so far.
59+
*/
60+
NONE,
61+
/**
62+
* The task execution has been started and is ongoing.
63+
*/
64+
STARTED,
65+
/**
66+
* The task execution finished successfully.
67+
*/
68+
SUCCESS,
69+
/**
70+
* The task execution finished with an error.
71+
*/
72+
ERROR
73+
}
74+
}

‎spring-context/src/test/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessorTests.java

+46-91
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import java.lang.annotation.Retention;
2222
import java.lang.annotation.RetentionPolicy;
2323
import java.lang.annotation.Target;
24-
import java.lang.reflect.Method;
2524
import java.time.Duration;
2625
import java.time.Instant;
2726
import java.time.ZoneId;
@@ -32,6 +31,7 @@
3231
import java.util.Properties;
3332
import java.util.concurrent.TimeUnit;
3433

34+
import org.assertj.core.api.AbstractAssert;
3535
import org.junit.jupiter.api.AfterEach;
3636
import org.junit.jupiter.api.Test;
3737
import org.junit.jupiter.api.extension.ParameterContext;
@@ -116,11 +116,7 @@ void fixedDelayTask(@NameToClass Class<?> beanClass, long expectedInterval) {
116116
new DirectFieldAccessor(registrar).getPropertyValue("fixedDelayTasks");
117117
assertThat(fixedDelayTasks).hasSize(1);
118118
IntervalTask task = fixedDelayTasks.get(0);
119-
ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable();
120-
Object targetObject = runnable.getTarget();
121-
Method targetMethod = runnable.getMethod();
122-
assertThat(targetObject).isEqualTo(target);
123-
assertThat(targetMethod.getName()).isEqualTo("fixedDelay");
119+
assertThatScheduledRunnable(task.getRunnable()).hasTarget(target).hasMethodName("fixedDelay");
124120
assertThat(task.getInitialDelayDuration()).isZero();
125121
assertThat(task.getIntervalDuration()).isEqualTo(
126122
Duration.ofMillis(expectedInterval < 0 ? Long.MAX_VALUE : expectedInterval));
@@ -150,11 +146,7 @@ void fixedRateTask(@NameToClass Class<?> beanClass, long expectedInterval) {
150146
new DirectFieldAccessor(registrar).getPropertyValue("fixedRateTasks");
151147
assertThat(fixedRateTasks).hasSize(1);
152148
IntervalTask task = fixedRateTasks.get(0);
153-
ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable();
154-
Object targetObject = runnable.getTarget();
155-
Method targetMethod = runnable.getMethod();
156-
assertThat(targetObject).isEqualTo(target);
157-
assertThat(targetMethod.getName()).isEqualTo("fixedRate");
149+
assertThatScheduledRunnable(task.getRunnable()).hasTarget(target).hasMethodName("fixedRate");
158150
assertSoftly(softly -> {
159151
softly.assertThat(task.getInitialDelayDuration()).as("initial delay").isZero();
160152
softly.assertThat(task.getIntervalDuration()).as("interval").isEqualTo(Duration.ofMillis(expectedInterval));
@@ -185,11 +177,7 @@ void fixedRateTaskWithInitialDelay(@NameToClass Class<?> beanClass, long expecte
185177
new DirectFieldAccessor(registrar).getPropertyValue("fixedRateTasks");
186178
assertThat(fixedRateTasks).hasSize(1);
187179
IntervalTask task = fixedRateTasks.get(0);
188-
ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable();
189-
Object targetObject = runnable.getTarget();
190-
Method targetMethod = runnable.getMethod();
191-
assertThat(targetObject).isEqualTo(target);
192-
assertThat(targetMethod.getName()).isEqualTo("fixedRate");
180+
assertThatScheduledRunnable(task.getRunnable()).hasTarget(target).hasMethodName("fixedRate");
193181
assertSoftly(softly -> {
194182
softly.assertThat(task.getInitialDelayDuration()).as("initial delay").isEqualTo(Duration.ofMillis(expectedInitialDelay));
195183
softly.assertThat(task.getIntervalDuration()).as("interval").isEqualTo(Duration.ofMillis(expectedInterval));
@@ -250,19 +238,11 @@ private void severalFixedRates(StaticApplicationContext context,
250238
new DirectFieldAccessor(registrar).getPropertyValue("fixedRateTasks");
251239
assertThat(fixedRateTasks).hasSize(2);
252240
IntervalTask task1 = fixedRateTasks.get(0);
253-
ScheduledMethodRunnable runnable1 = (ScheduledMethodRunnable) task1.getRunnable();
254-
Object targetObject = runnable1.getTarget();
255-
Method targetMethod = runnable1.getMethod();
256-
assertThat(targetObject).isEqualTo(target);
257-
assertThat(targetMethod.getName()).isEqualTo("fixedRate");
241+
assertThatScheduledRunnable(task1.getRunnable()).hasTarget(target).hasMethodName("fixedRate");
258242
assertThat(task1.getInitialDelayDuration()).isZero();
259243
assertThat(task1.getIntervalDuration()).isEqualTo(Duration.ofMillis(4_000L));
260244
IntervalTask task2 = fixedRateTasks.get(1);
261-
ScheduledMethodRunnable runnable2 = (ScheduledMethodRunnable) task2.getRunnable();
262-
targetObject = runnable2.getTarget();
263-
targetMethod = runnable2.getMethod();
264-
assertThat(targetObject).isEqualTo(target);
265-
assertThat(targetMethod.getName()).isEqualTo("fixedRate");
245+
assertThatScheduledRunnable(task2.getRunnable()).hasTarget(target).hasMethodName("fixedRate");
266246
assertThat(task2.getInitialDelayDuration()).isEqualTo(Duration.ofMillis(2_000L));
267247
assertThat(task2.getIntervalDuration()).isEqualTo(Duration.ofMillis(4_000L));
268248
}
@@ -286,11 +266,7 @@ void oneTimeTask() {
286266
new DirectFieldAccessor(registrar).getPropertyValue("oneTimeTasks");
287267
assertThat(oneTimeTasks).hasSize(1);
288268
OneTimeTask task = oneTimeTasks.get(0);
289-
ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable();
290-
Object targetObject = runnable.getTarget();
291-
Method targetMethod = runnable.getMethod();
292-
assertThat(targetObject).isEqualTo(target);
293-
assertThat(targetMethod.getName()).isEqualTo("oneTimeTask");
269+
assertThatScheduledRunnable(task.getRunnable()).hasTarget(target).hasMethodName("oneTimeTask");
294270
assertThat(task.getInitialDelayDuration()).isEqualTo(Duration.ofMillis(2_000L));
295271
}
296272

@@ -313,11 +289,7 @@ void cronTask() {
313289
new DirectFieldAccessor(registrar).getPropertyValue("cronTasks");
314290
assertThat(cronTasks).hasSize(1);
315291
CronTask task = cronTasks.get(0);
316-
ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable();
317-
Object targetObject = runnable.getTarget();
318-
Method targetMethod = runnable.getMethod();
319-
assertThat(targetObject).isEqualTo(target);
320-
assertThat(targetMethod.getName()).isEqualTo("cron");
292+
assertThatScheduledRunnable(task.getRunnable()).hasTarget(target).hasMethodName("cron");
321293
assertThat(task.getExpression()).isEqualTo("*/7 * * * * ?");
322294
}
323295

@@ -340,11 +312,7 @@ void cronTaskWithZone() {
340312
new DirectFieldAccessor(registrar).getPropertyValue("cronTasks");
341313
assertThat(cronTasks).hasSize(1);
342314
CronTask task = cronTasks.get(0);
343-
ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable();
344-
Object targetObject = runnable.getTarget();
345-
Method targetMethod = runnable.getMethod();
346-
assertThat(targetObject).isEqualTo(target);
347-
assertThat(targetMethod.getName()).isEqualTo("cron");
315+
assertThatScheduledRunnable(task.getRunnable()).hasTarget(target).hasMethodName("cron");
348316
assertThat(task.getExpression()).isEqualTo("0 0 0-4,6-23 * * ?");
349317
Trigger trigger = task.getTrigger();
350318
assertThat(trigger).isNotNull();
@@ -404,11 +372,9 @@ void cronTaskWithScopedProxy() {
404372
new DirectFieldAccessor(registrar).getPropertyValue("cronTasks");
405373
assertThat(cronTasks).hasSize(1);
406374
CronTask task = cronTasks.get(0);
407-
ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable();
408-
Object targetObject = runnable.getTarget();
409-
Method targetMethod = runnable.getMethod();
410-
assertThat(targetObject).isEqualTo(context.getBean(ScopedProxyUtils.getTargetBeanName("target")));
411-
assertThat(targetMethod.getName()).isEqualTo("cron");
375+
assertThatScheduledRunnable(task.getRunnable())
376+
.hasTarget(context.getBean(ScopedProxyUtils.getTargetBeanName("target")))
377+
.hasMethodName("cron");
412378
assertThat(task.getExpression()).isEqualTo("*/7 * * * * ?");
413379
}
414380

@@ -431,11 +397,7 @@ void metaAnnotationWithFixedRate() {
431397
new DirectFieldAccessor(registrar).getPropertyValue("fixedRateTasks");
432398
assertThat(fixedRateTasks).hasSize(1);
433399
IntervalTask task = fixedRateTasks.get(0);
434-
ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable();
435-
Object targetObject = runnable.getTarget();
436-
Method targetMethod = runnable.getMethod();
437-
assertThat(targetObject).isEqualTo(target);
438-
assertThat(targetMethod.getName()).isEqualTo("checkForUpdates");
400+
assertThatScheduledRunnable(task.getRunnable()).hasTarget(target).hasMethodName("checkForUpdates");
439401
assertThat(task.getIntervalDuration()).isEqualTo(Duration.ofMillis(5_000L));
440402
}
441403

@@ -458,11 +420,7 @@ void composedAnnotationWithInitialDelayAndFixedRate() {
458420
new DirectFieldAccessor(registrar).getPropertyValue("fixedRateTasks");
459421
assertThat(fixedRateTasks).hasSize(1);
460422
IntervalTask task = fixedRateTasks.get(0);
461-
ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable();
462-
Object targetObject = runnable.getTarget();
463-
Method targetMethod = runnable.getMethod();
464-
assertThat(targetObject).isEqualTo(target);
465-
assertThat(targetMethod.getName()).isEqualTo("checkForUpdates");
423+
assertThatScheduledRunnable(task.getRunnable()).hasTarget(target).hasMethodName("checkForUpdates");
466424
assertThat(task.getIntervalDuration()).isEqualTo(Duration.ofMillis(5_000L));
467425
assertThat(task.getInitialDelayDuration()).isEqualTo(Duration.ofMillis(1_000L));
468426
}
@@ -486,11 +444,7 @@ void metaAnnotationWithCronExpression() {
486444
new DirectFieldAccessor(registrar).getPropertyValue("cronTasks");
487445
assertThat(cronTasks).hasSize(1);
488446
CronTask task = cronTasks.get(0);
489-
ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable();
490-
Object targetObject = runnable.getTarget();
491-
Method targetMethod = runnable.getMethod();
492-
assertThat(targetObject).isEqualTo(target);
493-
assertThat(targetMethod.getName()).isEqualTo("generateReport");
447+
assertThatScheduledRunnable(task.getRunnable()).hasTarget(target).hasMethodName("generateReport");
494448
assertThat(task.getExpression()).isEqualTo("0 0 * * * ?");
495449
}
496450

@@ -519,11 +473,7 @@ void propertyPlaceholderWithCron() {
519473
new DirectFieldAccessor(registrar).getPropertyValue("cronTasks");
520474
assertThat(cronTasks).hasSize(1);
521475
CronTask task = cronTasks.get(0);
522-
ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable();
523-
Object targetObject = runnable.getTarget();
524-
Method targetMethod = runnable.getMethod();
525-
assertThat(targetObject).isEqualTo(target);
526-
assertThat(targetMethod.getName()).isEqualTo("x");
476+
assertThatScheduledRunnable(task.getRunnable()).hasTarget(target).hasMethodName("x");
527477
assertThat(task.getExpression()).isEqualTo(businessHoursCronExpression);
528478
}
529479

@@ -578,11 +528,7 @@ void propertyPlaceholderWithFixedDelay(@NameToClass Class<?> beanClass, String f
578528
new DirectFieldAccessor(registrar).getPropertyValue("fixedDelayTasks");
579529
assertThat(fixedDelayTasks).hasSize(1);
580530
IntervalTask task = fixedDelayTasks.get(0);
581-
ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable();
582-
Object targetObject = runnable.getTarget();
583-
Method targetMethod = runnable.getMethod();
584-
assertThat(targetObject).isEqualTo(target);
585-
assertThat(targetMethod.getName()).isEqualTo("fixedDelay");
531+
assertThatScheduledRunnable(task.getRunnable()).hasTarget(target).hasMethodName("fixedDelay");
586532
assertSoftly(softly -> {
587533
softly.assertThat(task.getInitialDelayDuration()).as("initial delay").isEqualTo(Duration.ofMillis(expectedInitialDelay));
588534
softly.assertThat(task.getIntervalDuration()).as("interval").isEqualTo(Duration.ofMillis(expectedInterval));
@@ -622,11 +568,7 @@ void propertyPlaceholderWithFixedRate(@NameToClass Class<?> beanClass, String fi
622568
new DirectFieldAccessor(registrar).getPropertyValue("fixedRateTasks");
623569
assertThat(fixedRateTasks).hasSize(1);
624570
IntervalTask task = fixedRateTasks.get(0);
625-
ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable();
626-
Object targetObject = runnable.getTarget();
627-
Method targetMethod = runnable.getMethod();
628-
assertThat(targetObject).isEqualTo(target);
629-
assertThat(targetMethod.getName()).isEqualTo("fixedRate");
571+
assertThatScheduledRunnable(task.getRunnable()).hasTarget(target).hasMethodName("fixedRate");
630572
assertSoftly(softly -> {
631573
softly.assertThat(task.getInitialDelayDuration()).as("initial delay").isEqualTo(Duration.ofMillis(expectedInitialDelay));
632574
softly.assertThat(task.getIntervalDuration()).as("interval").isEqualTo(Duration.ofMillis(expectedInterval));
@@ -656,11 +598,7 @@ void expressionWithCron() {
656598
new DirectFieldAccessor(registrar).getPropertyValue("cronTasks");
657599
assertThat(cronTasks).hasSize(1);
658600
CronTask task = cronTasks.get(0);
659-
ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable();
660-
Object targetObject = runnable.getTarget();
661-
Method targetMethod = runnable.getMethod();
662-
assertThat(targetObject).isEqualTo(target);
663-
assertThat(targetMethod.getName()).isEqualTo("x");
601+
assertThatScheduledRunnable(task.getRunnable()).hasTarget(target).hasMethodName("x");
664602
assertThat(task.getExpression()).isEqualTo(businessHoursCronExpression);
665603
}
666604

@@ -689,11 +627,7 @@ void propertyPlaceholderForMetaAnnotation() {
689627
new DirectFieldAccessor(registrar).getPropertyValue("cronTasks");
690628
assertThat(cronTasks).hasSize(1);
691629
CronTask task = cronTasks.get(0);
692-
ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable();
693-
Object targetObject = runnable.getTarget();
694-
Method targetMethod = runnable.getMethod();
695-
assertThat(targetObject).isEqualTo(target);
696-
assertThat(targetMethod.getName()).isEqualTo("y");
630+
assertThatScheduledRunnable(task.getRunnable()).hasTarget(target).hasMethodName("y");
697631
assertThat(task.getExpression()).isEqualTo(businessHoursCronExpression);
698632
}
699633

@@ -716,11 +650,7 @@ void nonVoidReturnType() {
716650
new DirectFieldAccessor(registrar).getPropertyValue("cronTasks");
717651
assertThat(cronTasks).hasSize(1);
718652
CronTask task = cronTasks.get(0);
719-
ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable();
720-
Object targetObject = runnable.getTarget();
721-
Method targetMethod = runnable.getMethod();
722-
assertThat(targetObject).isEqualTo(target);
723-
assertThat(targetMethod.getName()).isEqualTo("cron");
653+
assertThatScheduledRunnable(task.getRunnable()).hasTarget(target).hasMethodName("cron");
724654
assertThat(task.getExpression()).isEqualTo("0 0 9-17 * * MON-FRI");
725655
}
726656

@@ -1088,4 +1018,29 @@ public Class<?> convert(Object beanClassName, ParameterContext context) throws A
10881018
}
10891019
}
10901020

1021+
static ScheduledMethodRunnableAssert assertThatScheduledRunnable(Runnable runnable) {
1022+
return new ScheduledMethodRunnableAssert(runnable);
1023+
}
1024+
1025+
static class ScheduledMethodRunnableAssert extends AbstractAssert<ScheduledMethodRunnableAssert, Runnable> {
1026+
1027+
public ScheduledMethodRunnableAssert(Runnable actual) {
1028+
super(actual, ScheduledMethodRunnableAssert.class);
1029+
assertThat(actual).extracting("runnable").isInstanceOf(ScheduledMethodRunnable.class);
1030+
}
1031+
1032+
public ScheduledMethodRunnableAssert hasTarget(Object target) {
1033+
isNotNull();
1034+
assertThat(actual).extracting("runnable.target").isEqualTo(target);
1035+
return this;
1036+
}
1037+
1038+
public ScheduledMethodRunnableAssert hasMethodName(String name) {
1039+
isNotNull();
1040+
assertThat(actual).extracting("runnable.method.name").isEqualTo(name);
1041+
return this;
1042+
}
1043+
1044+
}
1045+
10911046
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.scheduling.config;
18+
19+
20+
import java.time.Duration;
21+
import java.time.Instant;
22+
23+
import org.awaitility.Awaitility;
24+
import org.junit.jupiter.api.AfterEach;
25+
import org.junit.jupiter.api.BeforeEach;
26+
import org.junit.jupiter.api.Test;
27+
28+
import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler;
29+
30+
import static org.assertj.core.api.Assertions.assertThat;
31+
32+
/**
33+
* Tests for {@link ScheduledTask}.
34+
* @author Brian Clozel
35+
*/
36+
class ScheduledTaskTests {
37+
38+
private CountingRunnable countingRunnable = new CountingRunnable();
39+
40+
private SimpleAsyncTaskScheduler taskScheduler = new SimpleAsyncTaskScheduler();
41+
42+
private ScheduledTaskRegistrar taskRegistrar = new ScheduledTaskRegistrar();
43+
44+
@BeforeEach
45+
void setup() {
46+
this.taskRegistrar.setTaskScheduler(this.taskScheduler);
47+
taskScheduler.start();
48+
}
49+
50+
@AfterEach
51+
void tearDown() {
52+
taskScheduler.stop();
53+
}
54+
55+
@Test
56+
void shouldReturnConfiguredTask() {
57+
Task task = new Task(countingRunnable);
58+
ScheduledTask scheduledTask = new ScheduledTask(task);
59+
assertThat(scheduledTask.getTask()).isEqualTo(task);
60+
}
61+
62+
@Test
63+
void shouldUseTaskToString() {
64+
Task task = new Task(countingRunnable);
65+
ScheduledTask scheduledTask = new ScheduledTask(task);
66+
assertThat(scheduledTask.toString()).isEqualTo(task.toString());
67+
}
68+
69+
@Test
70+
void unscheduledTaskShouldNotHaveNextExecution() {
71+
ScheduledTask scheduledTask = new ScheduledTask(new Task(countingRunnable));
72+
assertThat(scheduledTask.nextExecution()).isNull();
73+
assertThat(countingRunnable.executionCount).isZero();
74+
}
75+
76+
@Test
77+
void scheduledTaskShouldHaveNextExecution() {
78+
ScheduledTask scheduledTask = taskRegistrar.scheduleFixedDelayTask(new FixedDelayTask(countingRunnable,
79+
Duration.ofSeconds(10), Duration.ofSeconds(10)));
80+
assertThat(scheduledTask.nextExecution()).isBefore(Instant.now().plusSeconds(11));
81+
}
82+
83+
@Test
84+
void cancelledTaskShouldNotHaveNextExecution() {
85+
ScheduledTask scheduledTask = taskRegistrar.scheduleFixedDelayTask(new FixedDelayTask(countingRunnable,
86+
Duration.ofSeconds(10), Duration.ofSeconds(10)));
87+
scheduledTask.cancel(true);
88+
assertThat(scheduledTask.nextExecution()).isNull();
89+
}
90+
91+
@Test
92+
void singleExecutionShouldNotHaveNextExecution() {
93+
ScheduledTask scheduledTask = taskRegistrar.scheduleOneTimeTask(new OneTimeTask(countingRunnable, Duration.ofSeconds(0)));
94+
Awaitility.await().atMost(Duration.ofSeconds(5)).until(() -> countingRunnable.executionCount > 0);
95+
assertThat(scheduledTask.nextExecution()).isNull();
96+
}
97+
98+
class CountingRunnable implements Runnable {
99+
100+
int executionCount;
101+
102+
@Override
103+
public void run() {
104+
executionCount++;
105+
}
106+
}
107+
108+
}

‎spring-context/src/test/java/org/springframework/scheduling/config/ScheduledTasksBeanDefinitionParserTests.java

+9-6
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@
1616

1717
package org.springframework.scheduling.config;
1818

19-
import java.lang.reflect.Method;
2019
import java.time.Duration;
2120
import java.time.Instant;
2221
import java.util.List;
2322

23+
import org.assertj.core.api.ObjectAssert;
2424
import org.junit.jupiter.api.BeforeEach;
2525
import org.junit.jupiter.api.Test;
2626

@@ -32,6 +32,7 @@
3232
import org.springframework.scheduling.support.ScheduledMethodRunnable;
3333

3434
import static org.assertj.core.api.Assertions.assertThat;
35+
import static org.assertj.core.api.InstanceOfAssertFactories.type;
3536

3637
/**
3738
* @author Mark Fisher
@@ -68,11 +69,13 @@ void checkTarget() {
6869
List<IntervalTask> tasks = (List<IntervalTask>) new DirectFieldAccessor(
6970
this.registrar).getPropertyValue("fixedRateTasks");
7071
Runnable runnable = tasks.get(0).getRunnable();
71-
assertThat(runnable.getClass()).isEqualTo(ScheduledMethodRunnable.class);
72-
Object targetObject = ((ScheduledMethodRunnable) runnable).getTarget();
73-
Method targetMethod = ((ScheduledMethodRunnable) runnable).getMethod();
74-
assertThat(targetObject).isEqualTo(this.testBean);
75-
assertThat(targetMethod.getName()).isEqualTo("test");
72+
73+
ObjectAssert<ScheduledMethodRunnable> runnableAssert = assertThat(runnable)
74+
.extracting("runnable")
75+
.isInstanceOf(ScheduledMethodRunnable.class)
76+
.asInstanceOf(type(ScheduledMethodRunnable.class));
77+
runnableAssert.extracting("target").isEqualTo(testBean);
78+
runnableAssert.extracting("method.name").isEqualTo("test");
7679
}
7780

7881
@Test
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.scheduling.config;
18+
19+
20+
import java.time.Instant;
21+
22+
import org.junit.jupiter.api.Test;
23+
24+
import static org.assertj.core.api.Assertions.assertThat;
25+
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
26+
27+
/**
28+
* Tests for {@link TaskExecutionOutcome}.
29+
* @author Brian Clozel
30+
*/
31+
class TaskExecutionOutcomeTests {
32+
33+
@Test
34+
void shouldCreateWithNoneStatus() {
35+
TaskExecutionOutcome outcome = TaskExecutionOutcome.create();
36+
assertThat(outcome.status()).isEqualTo(TaskExecutionOutcome.Status.NONE);
37+
assertThat(outcome.executionTime()).isNull();
38+
assertThat(outcome.throwable()).isNull();
39+
}
40+
41+
@Test
42+
void startedTaskShouldBeOngoing() {
43+
TaskExecutionOutcome outcome = TaskExecutionOutcome.create();
44+
Instant now = Instant.now();
45+
outcome = outcome.start(now);
46+
assertThat(outcome.status()).isEqualTo(TaskExecutionOutcome.Status.STARTED);
47+
assertThat(outcome.executionTime()).isEqualTo(now);
48+
assertThat(outcome.throwable()).isNull();
49+
}
50+
51+
@Test
52+
void shouldRejectSuccessWhenNotStarted() {
53+
TaskExecutionOutcome outcome = TaskExecutionOutcome.create();
54+
assertThatIllegalStateException().isThrownBy(outcome::success);
55+
}
56+
57+
@Test
58+
void shouldRejectErrorWhenNotStarted() {
59+
TaskExecutionOutcome outcome = TaskExecutionOutcome.create();
60+
assertThatIllegalStateException().isThrownBy(() -> outcome.failure(new IllegalArgumentException("test error")));
61+
}
62+
63+
@Test
64+
void finishedTaskShouldBeSuccessful() {
65+
TaskExecutionOutcome outcome = TaskExecutionOutcome.create();
66+
Instant now = Instant.now();
67+
outcome = outcome.start(now);
68+
outcome = outcome.success();
69+
assertThat(outcome.status()).isEqualTo(TaskExecutionOutcome.Status.SUCCESS);
70+
assertThat(outcome.executionTime()).isEqualTo(now);
71+
assertThat(outcome.throwable()).isNull();
72+
}
73+
74+
@Test
75+
void errorTaskShouldBeFailure() {
76+
TaskExecutionOutcome outcome = TaskExecutionOutcome.create();
77+
Instant now = Instant.now();
78+
outcome = outcome.start(now);
79+
outcome = outcome.failure(new IllegalArgumentException(("test error")));
80+
assertThat(outcome.status()).isEqualTo(TaskExecutionOutcome.Status.ERROR);
81+
assertThat(outcome.executionTime()).isEqualTo(now);
82+
assertThat(outcome.throwable()).isInstanceOf(IllegalArgumentException.class);
83+
}
84+
85+
@Test
86+
void newTaskExecutionShouldNotFail() {
87+
TaskExecutionOutcome outcome = TaskExecutionOutcome.create();
88+
Instant now = Instant.now();
89+
outcome = outcome.start(now);
90+
outcome = outcome.failure(new IllegalArgumentException(("test error")));
91+
92+
outcome = outcome.start(now.plusSeconds(2));
93+
assertThat(outcome.status()).isEqualTo(TaskExecutionOutcome.Status.STARTED);
94+
assertThat(outcome.executionTime()).isAfter(now);
95+
assertThat(outcome.throwable()).isNull();
96+
}
97+
98+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.scheduling.config;
18+
19+
import org.junit.jupiter.api.Test;
20+
21+
import static org.assertj.core.api.Assertions.assertThat;
22+
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
23+
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
24+
25+
/**
26+
* Tests for {@link Task}.
27+
* @author Brian Clozel
28+
*/
29+
class TaskTests {
30+
31+
@Test
32+
void shouldRejectNullRunnable() {
33+
assertThatIllegalArgumentException().isThrownBy(() -> new Task(null));
34+
}
35+
36+
@Test
37+
void initialStateShouldBeUnknown() {
38+
TestRunnable testRunnable = new TestRunnable();
39+
Task task = new Task(testRunnable);
40+
assertThat(testRunnable.hasRun).isFalse();
41+
TaskExecutionOutcome executionOutcome = task.getLastExecutionOutcome();
42+
assertThat(executionOutcome.executionTime()).isNull();
43+
assertThat(executionOutcome.status()).isEqualTo(TaskExecutionOutcome.Status.NONE);
44+
assertThat(executionOutcome.throwable()).isNull();
45+
}
46+
47+
@Test
48+
void stateShouldUpdateAfterRun() {
49+
TestRunnable testRunnable = new TestRunnable();
50+
Task task = new Task(testRunnable);
51+
task.getRunnable().run();
52+
53+
assertThat(testRunnable.hasRun).isTrue();
54+
TaskExecutionOutcome executionOutcome = task.getLastExecutionOutcome();
55+
assertThat(executionOutcome.executionTime()).isInThePast();
56+
assertThat(executionOutcome.status()).isEqualTo(TaskExecutionOutcome.Status.SUCCESS);
57+
assertThat(executionOutcome.throwable()).isNull();
58+
}
59+
60+
@Test
61+
void stateShouldUpdateAfterFailingRun() {
62+
FailingTestRunnable testRunnable = new FailingTestRunnable();
63+
Task task = new Task(testRunnable);
64+
assertThatIllegalStateException().isThrownBy(() -> task.getRunnable().run());
65+
66+
assertThat(testRunnable.hasRun).isTrue();
67+
TaskExecutionOutcome executionOutcome = task.getLastExecutionOutcome();
68+
assertThat(executionOutcome.executionTime()).isInThePast();
69+
assertThat(executionOutcome.status()).isEqualTo(TaskExecutionOutcome.Status.ERROR);
70+
assertThat(executionOutcome.throwable()).isInstanceOf(IllegalStateException.class);
71+
}
72+
73+
74+
static class TestRunnable implements Runnable {
75+
76+
boolean hasRun;
77+
78+
@Override
79+
public void run() {
80+
this.hasRun = true;
81+
}
82+
}
83+
84+
static class FailingTestRunnable implements Runnable {
85+
86+
boolean hasRun;
87+
88+
@Override
89+
public void run() {
90+
this.hasRun = true;
91+
throw new IllegalStateException("test exception");
92+
}
93+
}
94+
95+
96+
}

0 commit comments

Comments
 (0)
Please sign in to comment.