From 8c694edec40cbdf38a68209502a9fde1d9248b4e Mon Sep 17 00:00:00 2001 From: Knut Wannheden Date: Tue, 29 Mar 2022 15:44:56 +0200 Subject: [PATCH] Provide a new `ApplicationNotRunning` predicate for `@Scheduled` The `@Scheduled` annotation's `skipExecutionIf` attribute can be used to skip individual executions in a schedule. Currently, Quarkus only provides the `Never` implementation, which is also the default (never skips an execution). Since the scheduler is initialized very early (priority `PLATFORM_BEFORE`) it can happen that executions get scheduled before the application has been fully started. An application can solve this by itself waiting to get notified with the `StartupEvent` or it can now use this new predicate: `@Scheduled(..., skipExecutionIf = ApplicationNotRunning.class)`. --- .../deployment/SchedulerProcessor.java | 2 +- .../ApplicationNotRunningPredicateTest.java | 61 +++++++++++++++++++ .../java/io/quarkus/scheduler/Scheduled.java | 27 ++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/ApplicationNotRunningPredicateTest.java diff --git a/extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/SchedulerProcessor.java b/extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/SchedulerProcessor.java index 8cbbe7aa318a3..03d185ed6dff6 100644 --- a/extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/SchedulerProcessor.java +++ b/extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/SchedulerProcessor.java @@ -103,7 +103,7 @@ public class SchedulerProcessor { @BuildStep void beans(Capabilities capabilities, BuildProducer additionalBeans) { if (capabilities.isMissing(Capability.QUARTZ)) { - additionalBeans.produce(new AdditionalBeanBuildItem(SimpleScheduler.class)); + additionalBeans.produce(new AdditionalBeanBuildItem(SimpleScheduler.class, Scheduled.ApplicationNotRunning.class)); } } diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/ApplicationNotRunningPredicateTest.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/ApplicationNotRunningPredicateTest.java new file mode 100644 index 0000000000000..f2950545e701e --- /dev/null +++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/ApplicationNotRunningPredicateTest.java @@ -0,0 +1,61 @@ +package io.quarkus.scheduler.test; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.enterprise.event.Observes; +import javax.interceptor.Interceptor; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Priority; +import io.quarkus.runtime.StartupEvent; +import io.quarkus.scheduler.FailedExecution; +import io.quarkus.scheduler.Scheduled; +import io.quarkus.scheduler.SuccessfulExecution; +import io.quarkus.test.QuarkusUnitTest; + +public class ApplicationNotRunningPredicateTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest().withApplicationRoot((jar) -> jar.addClasses(Jobs.class)); + + static final CountDownLatch SUCCESS_LATCH = new CountDownLatch(1); + static SuccessfulExecution successfulExecution; + static FailedExecution failedExecution; + + @Test + public void testTriggerErrorStatus() throws InterruptedException { + assertTrue(SUCCESS_LATCH.await(5, TimeUnit.SECONDS)); + assertNull(failedExecution); + } + + void observeSuccessfulExecution(@Observes SuccessfulExecution successfulExecution) { + ApplicationNotRunningPredicateTest.successfulExecution = successfulExecution; + SUCCESS_LATCH.countDown(); + } + + void observeFailedExecution(@Observes FailedExecution failedExecution) { + ApplicationNotRunningPredicateTest.failedExecution = failedExecution; + } + + static class Jobs { + + volatile boolean preStart; + + void started(@Observes @Priority(Interceptor.Priority.PLATFORM_BEFORE) StartupEvent event) { + preStart = true; + } + + @Scheduled(every = "0.2s", skipExecutionIf = Scheduled.ApplicationNotRunning.class) + void scheduleAfterStarted() { + if (!preStart) { + throw new IllegalStateException(); + } + } + } +} diff --git a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/Scheduled.java b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/Scheduled.java index d2f0aaa4de691..bfa309c359bf5 100644 --- a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/Scheduled.java +++ b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/Scheduled.java @@ -10,8 +10,12 @@ import java.time.Duration; import java.util.concurrent.TimeUnit; +import javax.enterprise.context.ApplicationScoped; import javax.enterprise.context.Dependent; +import javax.enterprise.event.Observes; +import io.quarkus.runtime.ShutdownEvent; +import io.quarkus.runtime.StartupEvent; import io.quarkus.scheduler.Scheduled.Schedules; /** @@ -201,4 +205,27 @@ public boolean test(ScheduledExecution execution) { } + /** + * Execution is skipped if the application is not running (either not started or already shutdown). + */ + @ApplicationScoped + class ApplicationNotRunning implements SkipPredicate { + + private volatile boolean running; + + void started(@Observes StartupEvent event) { + this.running = true; + } + + void shutdown(@Observes ShutdownEvent event) { + this.running = false; + } + + @Override + public boolean test(ScheduledExecution execution) { + return !running; + } + + } + }