From e8391f121e2d3512c1b8461655d28d25ce13c605 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 25 Jul 2024 09:26:50 +0200 Subject: [PATCH] Add execution metadata to scheduled tasks actuator endpoint As of spring-projects/spring-framework#24560, Spring provides additional metadata for scheduled tasks: * next execution time * last execution outcome (including status, time and raised exception) This commit leverages this information to enhance the existing `scheduledtasks` Actuator endpoint. Closes gh-17585 --- ...eduledTasksEndpointDocumentationTests.java | 47 +++- .../scheduling/ScheduledTasksEndpoint.java | 240 ++++++++++++------ .../ScheduledTasksEndpointTests.java | 24 ++ 3 files changed, 237 insertions(+), 74 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/ScheduledTasksEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/ScheduledTasksEndpointDocumentationTests.java index f195cb3a20..59f31a2112 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/ScheduledTasksEndpointDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/ScheduledTasksEndpointDocumentationTests.java @@ -27,11 +27,13 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.restdocs.payload.FieldDescriptor; +import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.scheduling.Trigger; import org.springframework.scheduling.TriggerContext; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.SchedulingConfigurer; +import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler; import org.springframework.scheduling.config.ScheduledTaskHolder; import static org.assertj.core.api.Assertions.assertThat; @@ -57,9 +59,12 @@ class ScheduledTasksEndpointDocumentationTests extends MockMvcEndpointDocumentat "com.example.Processor")), responseFields(fieldWithPath("cron").description("Cron tasks, if any."), targetFieldWithPrefix("cron.[]."), + nextExecutionWithPrefix("cron.[].").description("Time of the next scheduled execution."), fieldWithPath("cron.[].expression").description("Cron expression."), fieldWithPath("fixedDelay").description("Fixed delay tasks, if any."), targetFieldWithPrefix("fixedDelay.[]."), initialDelayWithPrefix("fixedDelay.[]."), + nextExecutionWithPrefix("fixedDelay.[].") + .description("Time of the next scheduled execution."), fieldWithPath("fixedDelay.[].interval") .description("Interval, in milliseconds, between the end of the last" + " execution and the start of the next."), @@ -68,9 +73,15 @@ class ScheduledTasksEndpointDocumentationTests extends MockMvcEndpointDocumentat fieldWithPath("fixedRate.[].interval") .description("Interval, in milliseconds, between the start of each execution."), initialDelayWithPrefix("fixedRate.[]."), + nextExecutionWithPrefix("fixedRate.[].") + .description("Time of the next scheduled execution."), fieldWithPath("custom").description("Tasks with custom triggers, if any."), targetFieldWithPrefix("custom.[]."), - fieldWithPath("custom.[].trigger").description("Trigger for the task.")))); + fieldWithPath("custom.[].trigger").description("Trigger for the task.")) + .andWithPrefix("*.[].", + fieldWithPath("lastExecution").description("Last execution of this task, if any.") + .optional()) + .andWithPrefix("*.[].lastExecution.", lastExecution()))); } private FieldDescriptor targetFieldWithPrefix(String prefix) { @@ -81,6 +92,22 @@ class ScheduledTasksEndpointDocumentationTests extends MockMvcEndpointDocumentat return fieldWithPath(prefix + "initialDelay").description("Delay, in milliseconds, before first execution."); } + private FieldDescriptor nextExecutionWithPrefix(String prefix) { + return fieldWithPath(prefix + "nextExecution.time").description("Time of the next scheduled execution."); + } + + private FieldDescriptor[] lastExecution() { + return new FieldDescriptor[] { + fieldWithPath("status").description("Status of the last execution (STARTED, SUCCESS, ERROR)."), + fieldWithPath("time").description("Time of the last execution.").type(JsonFieldType.STRING), + fieldWithPath("exception.type").description("Exception type thrown by the task, if any.") + .type(JsonFieldType.STRING) + .optional(), + fieldWithPath("exception.message").description("Message of the exception thrown by the task, if any.") + .type(JsonFieldType.STRING) + .optional() }; + } + @Configuration(proxyBeanMethods = false) @EnableScheduling @Import(BaseDocumentationConfiguration.class) @@ -96,7 +123,7 @@ class ScheduledTasksEndpointDocumentationTests extends MockMvcEndpointDocumentat } - @Scheduled(fixedDelay = 5000, initialDelay = 5000) + @Scheduled(fixedDelay = 5000, initialDelay = 0) void purge() { } @@ -108,7 +135,10 @@ class ScheduledTasksEndpointDocumentationTests extends MockMvcEndpointDocumentat @Bean SchedulingConfigurer schedulingConfigurer() { - return (registrar) -> registrar.addTriggerTask(new CustomTriggeredRunnable(), new CustomTrigger()); + return (registrar) -> { + registrar.setTaskScheduler(new TestTaskScheduler()); + registrar.addTriggerTask(new CustomTriggeredRunnable(), new CustomTrigger()); + }; } static class CustomTrigger implements Trigger { @@ -124,7 +154,18 @@ class ScheduledTasksEndpointDocumentationTests extends MockMvcEndpointDocumentat @Override public void run() { + throw new IllegalStateException("Failed while running custom task"); + } + } + + static class TestTaskScheduler extends SimpleAsyncTaskScheduler { + + TestTaskScheduler() { + setThreadNamePrefix("test-"); + // do not log task errors + setErrorHandler((throwable) -> { + }); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/scheduling/ScheduledTasksEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/scheduling/ScheduledTasksEndpoint.java index 98eff14a5d..666bad4dc8 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/scheduling/ScheduledTasksEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/scheduling/ScheduledTasksEndpoint.java @@ -17,14 +17,12 @@ package org.springframework.boot.actuate.scheduling; import java.time.Duration; +import java.time.Instant; import java.util.Collection; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.function.Function; -import java.util.stream.Collectors; import org.springframework.aot.hint.BindingReflectionHintsRegistrar; import org.springframework.aot.hint.RuntimeHints; @@ -42,15 +40,20 @@ import org.springframework.scheduling.config.IntervalTask; import org.springframework.scheduling.config.ScheduledTask; import org.springframework.scheduling.config.ScheduledTaskHolder; import org.springframework.scheduling.config.Task; +import org.springframework.scheduling.config.TaskExecutionOutcome; +import org.springframework.scheduling.config.TaskExecutionOutcome.Status; import org.springframework.scheduling.config.TriggerTask; import org.springframework.scheduling.support.CronTrigger; import org.springframework.scheduling.support.PeriodicTrigger; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; /** * {@link Endpoint @Endpoint} to expose information about an application's scheduled * tasks. * * @author Andy Wilkinson + * @author Brian Clozel * @since 2.0.0 */ @Endpoint(id = "scheduledtasks") @@ -65,12 +68,16 @@ public class ScheduledTasksEndpoint { @ReadOperation public ScheduledTasksDescriptor scheduledTasks() { - Map> descriptionsByType = this.scheduledTaskHolders.stream() - .flatMap((holder) -> holder.getScheduledTasks().stream()) - .map(ScheduledTask::getTask) - .map(TaskDescriptor::of) - .filter(Objects::nonNull) - .collect(Collectors.groupingBy(TaskDescriptor::getType)); + MultiValueMap descriptionsByType = new LinkedMultiValueMap<>(); + for (ScheduledTaskHolder holder : this.scheduledTaskHolders) { + for (ScheduledTask scheduledTask : holder.getScheduledTasks()) { + TaskType taskType = TaskType.forTask(scheduledTask); + if (taskType != null) { + TaskDescriptor descriptor = taskType.createDescriptor(scheduledTask); + descriptionsByType.add(descriptor.getType(), descriptor); + } + } + } return new ScheduledTasksDescriptor(descriptionsByType); } @@ -117,45 +124,16 @@ public class ScheduledTasksEndpoint { */ public abstract static class TaskDescriptor { - private static final Map, Function> DESCRIBERS = new LinkedHashMap<>(); - - static { - DESCRIBERS.put(FixedRateTask.class, (task) -> new FixedRateTaskDescriptor((FixedRateTask) task)); - DESCRIBERS.put(FixedDelayTask.class, (task) -> new FixedDelayTaskDescriptor((FixedDelayTask) task)); - DESCRIBERS.put(CronTask.class, (task) -> new CronTaskDescriptor((CronTask) task)); - DESCRIBERS.put(TriggerTask.class, (task) -> describeTriggerTask((TriggerTask) task)); - } - private final TaskType type; + private final ScheduledTask scheduledTask; + private final RunnableDescriptor runnable; - private static TaskDescriptor of(Task task) { - return DESCRIBERS.entrySet() - .stream() - .filter((entry) -> entry.getKey().isInstance(task)) - .map((entry) -> entry.getValue().apply(task)) - .findFirst() - .orElse(null); - } - - private static TaskDescriptor describeTriggerTask(TriggerTask triggerTask) { - Trigger trigger = triggerTask.getTrigger(); - if (trigger instanceof CronTrigger cronTrigger) { - return new CronTaskDescriptor(triggerTask, cronTrigger); - } - if (trigger instanceof PeriodicTrigger periodicTrigger) { - if (periodicTrigger.isFixedRate()) { - return new FixedRateTaskDescriptor(triggerTask, periodicTrigger); - } - return new FixedDelayTaskDescriptor(triggerTask, periodicTrigger); - } - return new CustomTriggerTaskDescriptor(triggerTask); - } - - protected TaskDescriptor(TaskType type, Runnable runnable) { + protected TaskDescriptor(ScheduledTask scheduledTask, TaskType type) { + this.scheduledTask = scheduledTask; this.type = type; - this.runnable = new RunnableDescriptor(runnable); + this.runnable = new RunnableDescriptor(scheduledTask.getTask().getRunnable()); } private TaskType getType() { @@ -166,6 +144,130 @@ public class ScheduledTasksEndpoint { return this.runnable; } + public final NextExecution getNextExecution() { + Instant nextExecution = this.scheduledTask.nextExecution(); + if (nextExecution != null) { + return new NextExecution(nextExecution); + } + return null; + } + + public final LastExecution getLastExecution() { + TaskExecutionOutcome lastExecutionOutcome = this.scheduledTask.getTask().getLastExecutionOutcome(); + if (lastExecutionOutcome.status() != Status.NONE) { + return new LastExecution(lastExecutionOutcome); + } + return null; + } + + } + + public static final class NextExecution { + + private final Instant time; + + public NextExecution(Instant time) { + this.time = time; + } + + public Instant getTime() { + return this.time; + } + + } + + public static final class LastExecution { + + private final TaskExecutionOutcome lastExecutionOutcome; + + private LastExecution(TaskExecutionOutcome lastExecutionOutcome) { + this.lastExecutionOutcome = lastExecutionOutcome; + } + + public Status getStatus() { + return this.lastExecutionOutcome.status(); + } + + public Instant getTime() { + return this.lastExecutionOutcome.executionTime(); + } + + public ExceptionInfo getException() { + Throwable throwable = this.lastExecutionOutcome.throwable(); + if (throwable != null) { + return new ExceptionInfo(throwable); + } + return null; + } + + } + + public static final class ExceptionInfo { + + private final Throwable throwable; + + private ExceptionInfo(Throwable throwable) { + this.throwable = throwable; + } + + public String getType() { + return this.throwable.getClass().getName(); + } + + public String getMessage() { + return this.throwable.getMessage(); + } + + } + + private enum TaskType { + + CRON(CronTask.class, + (scheduledTask) -> new CronTaskDescriptor(scheduledTask, (CronTask) scheduledTask.getTask())), + FIXED_DELAY(FixedDelayTask.class, + (scheduledTask) -> new FixedDelayTaskDescriptor(scheduledTask, + (FixedDelayTask) scheduledTask.getTask())), + FIXED_RATE(FixedRateTask.class, + (scheduledTask) -> new FixedRateTaskDescriptor(scheduledTask, (FixedRateTask) scheduledTask.getTask())), + CUSTOM_TRIGGER(TriggerTask.class, TaskType::describeTriggerTask); + + final Class taskClass; + + final Function describer; + + TaskType(Class taskClass, Function describer) { + this.taskClass = taskClass; + this.describer = describer; + } + + static TaskType forTask(ScheduledTask scheduledTask) { + for (TaskType taskType : TaskType.values()) { + if (taskType.taskClass.isInstance(scheduledTask.getTask())) { + return taskType; + } + } + return null; + } + + TaskDescriptor createDescriptor(ScheduledTask scheduledTask) { + return this.describer.apply(scheduledTask); + } + + private static TaskDescriptor describeTriggerTask(ScheduledTask scheduledTask) { + TriggerTask triggerTask = (TriggerTask) scheduledTask.getTask(); + Trigger trigger = triggerTask.getTrigger(); + if (trigger instanceof CronTrigger cronTrigger) { + return new CronTaskDescriptor(scheduledTask, triggerTask, cronTrigger); + } + if (trigger instanceof PeriodicTrigger periodicTrigger) { + if (periodicTrigger.isFixedRate()) { + return new FixedRateTaskDescriptor(scheduledTask, triggerTask, periodicTrigger); + } + return new FixedDelayTaskDescriptor(scheduledTask, triggerTask, periodicTrigger); + } + return new CustomTriggerTaskDescriptor(scheduledTask); + } + } /** @@ -177,14 +279,15 @@ public class ScheduledTasksEndpoint { private final long interval; - protected IntervalTaskDescriptor(TaskType type, IntervalTask task) { - super(type, task.getRunnable()); - this.initialDelay = task.getInitialDelayDuration().toMillis(); - this.interval = task.getIntervalDuration().toMillis(); + protected IntervalTaskDescriptor(ScheduledTask scheduledTask, TaskType type, IntervalTask intervalTask) { + super(scheduledTask, type); + this.initialDelay = intervalTask.getInitialDelayDuration().toMillis(); + this.interval = intervalTask.getIntervalDuration().toMillis(); } - protected IntervalTaskDescriptor(TaskType type, TriggerTask task, PeriodicTrigger trigger) { - super(type, task.getRunnable()); + protected IntervalTaskDescriptor(ScheduledTask scheduledTask, TaskType type, TriggerTask task, + PeriodicTrigger trigger) { + super(scheduledTask, type); Duration initialDelayDuration = trigger.getInitialDelayDuration(); this.initialDelay = (initialDelayDuration != null) ? initialDelayDuration.toMillis() : 0; this.interval = trigger.getPeriodDuration().toMillis(); @@ -206,12 +309,12 @@ public class ScheduledTasksEndpoint { */ public static final class FixedDelayTaskDescriptor extends IntervalTaskDescriptor { - private FixedDelayTaskDescriptor(FixedDelayTask task) { - super(TaskType.FIXED_DELAY, task); + private FixedDelayTaskDescriptor(ScheduledTask scheduledTask, FixedDelayTask task) { + super(scheduledTask, TaskType.FIXED_DELAY, task); } - private FixedDelayTaskDescriptor(TriggerTask task, PeriodicTrigger trigger) { - super(TaskType.FIXED_DELAY, task, trigger); + private FixedDelayTaskDescriptor(ScheduledTask scheduledTask, TriggerTask task, PeriodicTrigger trigger) { + super(scheduledTask, TaskType.FIXED_DELAY, task, trigger); } } @@ -222,12 +325,12 @@ public class ScheduledTasksEndpoint { */ public static final class FixedRateTaskDescriptor extends IntervalTaskDescriptor { - private FixedRateTaskDescriptor(FixedRateTask task) { - super(TaskType.FIXED_RATE, task); + private FixedRateTaskDescriptor(ScheduledTask scheduledTask, FixedRateTask task) { + super(scheduledTask, TaskType.FIXED_RATE, task); } - private FixedRateTaskDescriptor(TriggerTask task, PeriodicTrigger trigger) { - super(TaskType.FIXED_RATE, task, trigger); + private FixedRateTaskDescriptor(ScheduledTask scheduledTask, TriggerTask task, PeriodicTrigger trigger) { + super(scheduledTask, TaskType.FIXED_RATE, task, trigger); } } @@ -240,13 +343,13 @@ public class ScheduledTasksEndpoint { private final String expression; - private CronTaskDescriptor(CronTask task) { - super(TaskType.CRON, task.getRunnable()); - this.expression = task.getExpression(); + private CronTaskDescriptor(ScheduledTask scheduledTask, CronTask cronTask) { + super(scheduledTask, TaskType.CRON); + this.expression = cronTask.getExpression(); } - private CronTaskDescriptor(TriggerTask task, CronTrigger trigger) { - super(TaskType.CRON, task.getRunnable()); + private CronTaskDescriptor(ScheduledTask scheduledTask, TriggerTask triggerTask, CronTrigger trigger) { + super(scheduledTask, TaskType.CRON); this.expression = trigger.getExpression(); } @@ -263,9 +366,10 @@ public class ScheduledTasksEndpoint { private final String trigger; - private CustomTriggerTaskDescriptor(TriggerTask task) { - super(TaskType.CUSTOM_TRIGGER, task.getRunnable()); - this.trigger = task.getTrigger().toString(); + private CustomTriggerTaskDescriptor(ScheduledTask scheduledTask) { + super(scheduledTask, TaskType.CUSTOM_TRIGGER); + TriggerTask triggerTask = (TriggerTask) scheduledTask.getTask(); + this.trigger = triggerTask.getTrigger().toString(); } public String getTrigger() { @@ -291,12 +395,6 @@ public class ScheduledTasksEndpoint { } - private enum TaskType { - - CRON, CUSTOM_TRIGGER, FIXED_DELAY, FIXED_RATE - - } - static class ScheduledTasksEndpointRuntimeHints implements RuntimeHintsRegistrar { private final BindingReflectionHintsRegistrar bindingRegistrar = new BindingReflectionHintsRegistrar(); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/scheduling/ScheduledTasksEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/scheduling/ScheduledTasksEndpointTests.java index 655c83fba8..61677e4e40 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/scheduling/ScheduledTasksEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/scheduling/ScheduledTasksEndpointTests.java @@ -31,8 +31,10 @@ import org.springframework.boot.actuate.scheduling.ScheduledTasksEndpoint.CronTa import org.springframework.boot.actuate.scheduling.ScheduledTasksEndpoint.CustomTriggerTaskDescriptor; import org.springframework.boot.actuate.scheduling.ScheduledTasksEndpoint.FixedDelayTaskDescriptor; import org.springframework.boot.actuate.scheduling.ScheduledTasksEndpoint.FixedRateTaskDescriptor; +import org.springframework.boot.actuate.scheduling.ScheduledTasksEndpoint.LastExecution; import org.springframework.boot.actuate.scheduling.ScheduledTasksEndpoint.ScheduledTasksDescriptor; import org.springframework.boot.actuate.scheduling.ScheduledTasksEndpoint.ScheduledTasksEndpointRuntimeHints; +import org.springframework.boot.actuate.scheduling.ScheduledTasksEndpoint.TaskDescriptor; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -42,6 +44,7 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.SchedulingConfigurer; import org.springframework.scheduling.config.ScheduledTaskHolder; import org.springframework.scheduling.config.ScheduledTaskRegistrar; +import org.springframework.scheduling.config.TaskExecutionOutcome.Status; import org.springframework.scheduling.support.CronTrigger; import org.springframework.scheduling.support.PeriodicTrigger; @@ -52,6 +55,7 @@ import static org.assertj.core.api.Assertions.assertThat; * * @author Andy Wilkinson * @author Moritz Halbritter + * @author Brian Clozel */ class ScheduledTasksEndpointTests { @@ -68,6 +72,8 @@ class ScheduledTasksEndpointTests { CronTaskDescriptor description = (CronTaskDescriptor) tasks.getCron().get(0); assertThat(description.getExpression()).isEqualTo("0 0 0/3 1/1 * ?"); assertThat(description.getRunnable().getTarget()).isEqualTo(CronScheduledMethod.class.getName() + ".cron"); + assertThat(description.getNextExecution().getTime()).isInTheFuture(); + assertThat(description.getLastExecution()).isNull(); }); } @@ -81,6 +87,7 @@ class ScheduledTasksEndpointTests { CronTaskDescriptor description = (CronTaskDescriptor) tasks.getCron().get(0); assertThat(description.getExpression()).isEqualTo("0 0 0/6 1/1 * ?"); assertThat(description.getRunnable().getTarget()).contains(CronTriggerRunnable.class.getName()); + assertThat(description.getLastExecution()).isNull(); }); } @@ -96,6 +103,7 @@ class ScheduledTasksEndpointTests { assertThat(description.getInterval()).isOne(); assertThat(description.getRunnable().getTarget()) .isEqualTo(FixedDelayScheduledMethod.class.getName() + ".fixedDelay"); + assertThat(description.getLastExecution()).isNull(); }); } @@ -110,6 +118,7 @@ class ScheduledTasksEndpointTests { assertThat(description.getInitialDelay()).isEqualTo(2000); assertThat(description.getInterval()).isEqualTo(1000); assertThat(description.getRunnable().getTarget()).contains(FixedDelayTriggerRunnable.class.getName()); + assertThat(description.getLastExecution()).isNull(); }); } @@ -124,6 +133,7 @@ class ScheduledTasksEndpointTests { assertThat(description.getInitialDelay()).isEqualTo(0); assertThat(description.getInterval()).isEqualTo(1000); assertThat(description.getRunnable().getTarget()).contains(FixedDelayTriggerRunnable.class.getName()); + assertThatTaskMayHaveBeenExecuted(description); }); } @@ -139,6 +149,7 @@ class ScheduledTasksEndpointTests { assertThat(description.getInterval()).isEqualTo(3); assertThat(description.getRunnable().getTarget()) .isEqualTo(FixedRateScheduledMethod.class.getName() + ".fixedRate"); + assertThat(description.getLastExecution()).isNull(); }); } @@ -153,6 +164,7 @@ class ScheduledTasksEndpointTests { assertThat(description.getInitialDelay()).isEqualTo(3000); assertThat(description.getInterval()).isEqualTo(2000); assertThat(description.getRunnable().getTarget()).contains(FixedRateTriggerRunnable.class.getName()); + assertThat(description.getLastExecution()).isNull(); }); } @@ -167,6 +179,7 @@ class ScheduledTasksEndpointTests { assertThat(description.getInitialDelay()).isEqualTo(0); assertThat(description.getInterval()).isEqualTo(2000); assertThat(description.getRunnable().getTarget()).contains(FixedRateTriggerRunnable.class.getName()); + assertThatTaskMayHaveBeenExecuted(description); }); } @@ -180,6 +193,7 @@ class ScheduledTasksEndpointTests { CustomTriggerTaskDescriptor description = (CustomTriggerTaskDescriptor) tasks.getCustom().get(0); assertThat(description.getRunnable().getTarget()).contains(CustomTriggerRunnable.class.getName()); assertThat(description.getTrigger()).isEqualTo(CustomTriggerTask.trigger.toString()); + assertThatTaskMayHaveBeenExecuted(description); }); } @@ -197,6 +211,16 @@ class ScheduledTasksEndpointTests { } } + private void assertThatTaskMayHaveBeenExecuted(TaskDescriptor descriptor) { + LastExecution lastExecution = descriptor.getLastExecution(); + if (lastExecution != null) { + if (lastExecution.getStatus() == Status.SUCCESS) { + assertThat(lastExecution.getTime()).isInThePast(); + assertThat(lastExecution.getException()).isNull(); + } + } + } + private void run(Class configuration, Consumer consumer) { this.contextRunner.withUserConfiguration(configuration) .run((context) -> consumer.accept(context.getBean(ScheduledTasksEndpoint.class).scheduledTasks()));