diff --git a/docs/src/main/asciidoc/scheduler-reference.adoc b/docs/src/main/asciidoc/scheduler-reference.adoc index 5c5310889fd17..4a9d53d25f4b4 100644 --- a/docs/src/main/asciidoc/scheduler-reference.adoc +++ b/docs/src/main/asciidoc/scheduler-reference.adoc @@ -199,6 +199,61 @@ void nonConcurrent() { ---- <1> Concurrent executions are skipped. +TIP: A CDI event of type `io.quarkus.scheduler.SkippedExecution` is fired when an execution of a scheduled method is skipped. + +[[conditional_execution]] +=== Conditional Execution + +You can define the logic to skip any execution of a scheduled method via `@Scheduled#skipExecutionIf()`. +The specified bean class must implement `io.quarkus.scheduler.Scheduled.SkipPredicate` and the execution is skipped if the result of the `test()` method is `true`. + +[source,java] +---- +class Jobs { + + @Scheduled(every = "1s", skipExecutionIf = MyPredicate.class) <1> + void everySecond() { + // do something every second... + } +} + +@Singleton <2> +class MyPredicate implements SkipPredicate { + + @Inject + MyService service; + + boolean test(ScheduledExecution execution) { + return !service.isStarted(); <3> + } +} +---- +<1> A bean instance of `MyPredicate.class` is used to evaluate whether an execution should be skipped. There must be exactly one bean that has the specified class in its set of bean types, otherwise the build fails. +<2> The scope of the bean must be active during execution. +<3> `Jobs.everySecond()` is skipped until `MyService.isStarted()` returns `true`. + +Note that this is an equivalent of the following code: + +[source,java] +---- +class Jobs { + + @Inject + MyService service; + + @Scheduled(every = "1s") + void everySecond() { + if (service.isStarted()) { + // do something every second... + } + } +} +---- + +The main idea is to keep the the logic to skip the execution outside the scheduled business methods so that it can be reused and refactored easily. + +TIP: A CDI event of type `io.quarkus.scheduler.SkippedExecution` is fired when an execution of a scheduled method is skipped. + == Scheduler Quarkus provides a built-in bean of type `io.quarkus.scheduler.Scheduler` that can be injected and used to pause/resume the scheduler and individual scheduled methods identified by a specific `Scheduled#identity()`. diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/ConditionalExecutionTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/ConditionalExecutionTest.java new file mode 100644 index 0000000000000..ffee6f7d8235c --- /dev/null +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/ConditionalExecutionTest.java @@ -0,0 +1,80 @@ +package io.quarkus.quartz.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.enterprise.event.Observes; +import javax.inject.Singleton; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.scheduler.Scheduled; +import io.quarkus.scheduler.ScheduledExecution; +import io.quarkus.scheduler.SkippedExecution; +import io.quarkus.test.QuarkusUnitTest; + +public class ConditionalExecutionTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(Jobs.class)); + + @Test + public void testExecution() { + try { + // Wait until Jobs#doSomething() is executed at least 1x and skipped 1x + if (IsDisabled.SKIPPED_LATCH.await(10, TimeUnit.SECONDS)) { + assertEquals(1, Jobs.COUNTER.getCount()); + IsDisabled.DISABLED.set(false); + } else { + fail("Job#foo not skipped in 10 seconds!"); + } + if (!Jobs.COUNTER.await(10, TimeUnit.SECONDS)) { + fail("Job#foo not executed in 10 seconds!"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(e); + } + } + + static class Jobs { + + static final CountDownLatch COUNTER = new CountDownLatch(1); + + @Scheduled(identity = "foo", every = "1s", skipExecutionIf = IsDisabled.class) + void doSomething() throws InterruptedException { + COUNTER.countDown(); + } + + } + + @Singleton + public static class IsDisabled implements Scheduled.SkipPredicate { + + static final CountDownLatch SKIPPED_LATCH = new CountDownLatch(1); + + static final AtomicBoolean DISABLED = new AtomicBoolean(true); + + @Override + public boolean test(ScheduledExecution execution) { + return DISABLED.get(); + } + + void onSkip(@Observes SkippedExecution event) { + if (event.triggerId.equals("foo")) { + System.out.println(event); + SKIPPED_LATCH.countDown(); + } + } + + } +} diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzScheduler.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzScheduler.java index c6988beb0101a..a71c1278f9bfd 100644 --- a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzScheduler.java +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzScheduler.java @@ -14,6 +14,7 @@ import javax.enterprise.context.BeforeDestroyed; import javax.enterprise.event.Event; import javax.enterprise.event.Observes; +import javax.enterprise.inject.Any; import javax.enterprise.inject.Instance; import javax.enterprise.inject.Produces; import javax.inject.Singleton; @@ -46,6 +47,7 @@ import com.cronutils.model.definition.CronDefinitionBuilder; import com.cronutils.parser.CronParser; +import io.quarkus.arc.Arc; import io.quarkus.runtime.StartupEvent; import io.quarkus.scheduler.Scheduled; import io.quarkus.scheduler.Scheduled.ConcurrentExecution; @@ -58,6 +60,7 @@ import io.quarkus.scheduler.runtime.SchedulerContext; import io.quarkus.scheduler.runtime.SchedulerRuntimeConfig; import io.quarkus.scheduler.runtime.SkipConcurrentExecutionInvoker; +import io.quarkus.scheduler.runtime.SkipPredicateInvoker; import io.quarkus.scheduler.runtime.util.SchedulerUtils; @Singleton @@ -126,6 +129,11 @@ public QuartzScheduler(SchedulerContext context, QuartzSupport quartzSupport, Sc if (scheduled.concurrentExecution() == ConcurrentExecution.SKIP) { invoker = new SkipConcurrentExecutionInvoker(invoker, skippedExecutionEvent); } + if (!scheduled.skipExecutionIf().equals(Scheduled.Never.class)) { + invoker = new SkipPredicateInvoker(invoker, + Arc.container().select(scheduled.skipExecutionIf(), Any.Literal.INSTANCE).get(), + skippedExecutionEvent); + } invokers.put(identity, invoker); JobBuilder jobBuilder = JobBuilder.newJob(InvokerJob.class) @@ -169,7 +177,7 @@ public QuartzScheduler(SchedulerContext context, QuartzSupport quartzSupport, Sc } TriggerBuilder triggerBuilder = TriggerBuilder.newTrigger() - .withIdentity(identity + "_trigger", Scheduler.class.getName()) + .withIdentity(identity, Scheduler.class.getName()) .withSchedule(scheduleBuilder); Long millisToAdd = null; @@ -416,7 +424,7 @@ static class QuartzTrigger implements Trigger { final JobExecutionContext context; - public QuartzTrigger(JobExecutionContext context) { + QuartzTrigger(JobExecutionContext context) { this.context = context; } @@ -434,7 +442,7 @@ public Instant getPreviousFireTime() { @Override public String getId() { - return context.getTrigger().getKey().toString(); + return context.getTrigger().getKey().getName(); } } @@ -443,7 +451,7 @@ static class QuartzScheduledExecution implements ScheduledExecution { final QuartzTrigger trigger; - public QuartzScheduledExecution(QuartzTrigger trigger) { + QuartzScheduledExecution(QuartzTrigger trigger) { this.trigger = trigger; } 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 10d9c0a7a5005..eff47c990383f 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 @@ -40,6 +40,7 @@ import io.quarkus.arc.deployment.UnremovableBeanBuildItem.BeanClassAnnotationExclusion; import io.quarkus.arc.deployment.ValidationPhaseBuildItem; import io.quarkus.arc.deployment.ValidationPhaseBuildItem.ValidationErrorBuildItem; +import io.quarkus.arc.processor.BeanDeploymentValidator; import io.quarkus.arc.processor.BeanInfo; import io.quarkus.arc.processor.BuiltinScope; import io.quarkus.arc.processor.DotNames; @@ -85,6 +86,7 @@ public class SchedulerProcessor { static final DotName SCHEDULED_NAME = DotName.createSimple(Scheduled.class.getName()); static final DotName SCHEDULES_NAME = DotName.createSimple(Scheduled.Schedules.class.getName()); + static final DotName SKIP_NEVER_NAME = DotName.createSimple(Scheduled.Never.class.getName()); static final Type SCHEDULED_EXECUTION_TYPE = Type.create(DotName.createSimple(ScheduledExecution.class.getName()), Kind.CLASS); @@ -184,7 +186,7 @@ void validateScheduledBusinessMethods(SchedulerConfig config, List encounteredIdentities) { + Map encounteredIdentities, + BeanDeploymentValidator.ValidationContext validationContext) { MethodInfo method = schedule.target().asMethod(); AnnotationValue cronValue = schedule.value("cron"); AnnotationValue everyValue = schedule.value("every"); @@ -343,6 +346,7 @@ private Throwable validateScheduled(CronParser parser, AnnotationInstance schedu schedule, method.declaringClass().name(), method.name()); } } + } else { if (everyValue != null && !everyValue.asString().trim().isEmpty()) { String every = everyValue.asString().trim(); @@ -375,6 +379,7 @@ private Throwable validateScheduled(CronParser parser, AnnotationInstance schedu return new IllegalStateException("Invalid delayed() expression on: " + schedule, e); } } + } } else { if (delayedValue != null && !delayedValue.asString().trim().isEmpty()) { @@ -395,8 +400,19 @@ private Throwable validateScheduled(CronParser parser, AnnotationInstance schedu } else { encounteredIdentities.put(identity, schedule); } + } + AnnotationValue skipExecutionIfValue = schedule.value("skipExecutionIf"); + if (skipExecutionIfValue != null) { + DotName skipPredicate = skipExecutionIfValue.asClass().name(); + if (!SKIP_NEVER_NAME.equals(skipPredicate) + && validationContext.beans().withBeanType(skipPredicate).collect().size() != 1) { + String message = String.format("There must be exactly one bean that matches the skip predicate: \"%s\" on: %s", + skipPredicate, schedule); + return new IllegalStateException(message); + } } + return null; } diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/ConditionalExecutionTest.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/ConditionalExecutionTest.java new file mode 100644 index 0000000000000..6f7e0b9eab131 --- /dev/null +++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/ConditionalExecutionTest.java @@ -0,0 +1,80 @@ +package io.quarkus.scheduler.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.enterprise.event.Observes; +import javax.inject.Singleton; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.scheduler.Scheduled; +import io.quarkus.scheduler.ScheduledExecution; +import io.quarkus.scheduler.SkippedExecution; +import io.quarkus.test.QuarkusUnitTest; + +public class ConditionalExecutionTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(Jobs.class)); + + @Test + public void testExecution() { + try { + // Wait until Jobs#doSomething() is executed at least 1x and skipped 1x + if (IsDisabled.SKIPPED_LATCH.await(10, TimeUnit.SECONDS)) { + assertEquals(1, Jobs.COUNTER.getCount()); + IsDisabled.DISABLED.set(false); + } else { + fail("Job#foo not skipped in 10 seconds!"); + } + if (!Jobs.COUNTER.await(10, TimeUnit.SECONDS)) { + fail("Job#foo not executed in 10 seconds!"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(e); + } + } + + static class Jobs { + + static final CountDownLatch COUNTER = new CountDownLatch(1); + + @Scheduled(identity = "foo", every = "1s", skipExecutionIf = IsDisabled.class) + void doSomething() throws InterruptedException { + COUNTER.countDown(); + } + + } + + @Singleton + public static class IsDisabled implements Scheduled.SkipPredicate { + + static final CountDownLatch SKIPPED_LATCH = new CountDownLatch(1); + + static final AtomicBoolean DISABLED = new AtomicBoolean(true); + + @Override + public boolean test(ScheduledExecution execution) { + return DISABLED.get(); + } + + void onSkip(@Observes SkippedExecution event) { + if (event.triggerId.equals("foo")) { + + SKIPPED_LATCH.countDown(); + } + } + + } +} diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidConditionalExecutionTest.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidConditionalExecutionTest.java new file mode 100644 index 0000000000000..f20795967eecd --- /dev/null +++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidConditionalExecutionTest.java @@ -0,0 +1,47 @@ +package io.quarkus.scheduler.test; + +import static org.junit.jupiter.api.Assertions.fail; + +import javax.enterprise.inject.spi.DeploymentException; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.scheduler.Scheduled; +import io.quarkus.scheduler.ScheduledExecution; +import io.quarkus.test.QuarkusUnitTest; + +public class InvalidConditionalExecutionTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .setExpectedException(DeploymentException.class) + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(Jobs.class, Some.class)); + + @Test + public void testExecution() { + fail(); + } + + static class Jobs { + + // This is wrong - Some.class is not a bean + @Scheduled(identity = "foo", every = "1s", skipExecutionIf = Some.class) + void doSomething() throws InterruptedException { + } + + } + + static class Some implements Scheduled.SkipPredicate { + + @Override + public boolean test(ScheduledExecution execution) { + return false; + } + + } + +} 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 e3d6873b99c1f..401d52d0dc2b5 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,6 +10,8 @@ import java.time.Duration; import java.util.concurrent.TimeUnit; +import javax.enterprise.context.Dependent; + import io.quarkus.scheduler.Scheduled.Schedules; /** @@ -34,7 +36,6 @@ * The annotated method must return {@code void} and either declare no parameters or one parameter of type * {@link ScheduledExecution}. * - * @author Martin Kouba * @see ScheduledExecution */ @Target(METHOD) @@ -119,6 +120,17 @@ */ ConcurrentExecution concurrentExecution() default PROCEED; + /** + * Specify the bean class that can be used to skip any execution of a scheduled method. + *

+ * There must be exactly one bean that has the specified class in its set of bean types, otherwise the build + * fails. Furthermore, the scope of the bean must be active during execution. If the scope is {@link Dependent} then the + * bean instance belongs exclusively to the specific scheduled method and is destroyed when the application is shut down. + * + * @return the bean class + */ + Class skipExecutionIf() default Never.class; + @Retention(RUNTIME) @Target(METHOD) @interface Schedules { @@ -145,4 +157,31 @@ enum ConcurrentExecution { } + /** + * + * @see Scheduled#skipExecutionIf() + */ + interface SkipPredicate { + + /** + * + * @param execution + * @return {@code true} if the given execution should be skipped, {@code false} otherwise + */ + boolean test(ScheduledExecution execution); + + } + + /** + * Execution is never skipped. + */ + class Never implements SkipPredicate { + + @Override + public boolean test(ScheduledExecution execution) { + return false; + } + + } + } diff --git a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/ScheduledExecution.java b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/ScheduledExecution.java index 2b1209355838f..4dd7d401f5f94 100644 --- a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/ScheduledExecution.java +++ b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/ScheduledExecution.java @@ -4,8 +4,6 @@ /** * Scheduled execution metadata. - * - * @author Martin Kouba */ public interface ScheduledExecution { diff --git a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/SkippedExecution.java b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/SkippedExecution.java index a31e13a1082fe..9d5ca9d94d56e 100644 --- a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/SkippedExecution.java +++ b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/SkippedExecution.java @@ -3,19 +3,48 @@ import java.time.Instant; /** - * A CDI event that is fired synchronously and asynchronously when a concurrent execution of a scheduled method is skipped. + * This event is fired synchronously and asynchronously when an execution of a scheduled method is skipped. * * @see io.quarkus.scheduler.Scheduled.ConcurrentExecution#SKIP + * @see io.quarkus.scheduler.Scheduled#skipExecutionIf() */ public class SkippedExecution { + // Keep the fields in order to minimize the breaking changes public final String triggerId; - public final Instant fireTime; - public SkippedExecution(String triggerId, Instant fireTime) { - this.triggerId = triggerId; - this.fireTime = fireTime; + private final ScheduledExecution execution; + private final String detail; + + public SkippedExecution(ScheduledExecution execution) { + this(execution, null); + } + + public SkippedExecution(ScheduledExecution execution, String detail) { + this.execution = execution; + this.detail = detail; + this.triggerId = execution.getTrigger().getId(); + this.fireTime = execution.getFireTime(); + } + + public ScheduledExecution getExecution() { + return execution; + } + + public String getDetail() { + return detail; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("Skipped execution of [") + .append(execution.getTrigger().getId()) + .append("]"); + if (detail != null) { + builder.append(": ").append(detail); + } + return builder.toString(); } } diff --git a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/Trigger.java b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/Trigger.java index a73225add437c..e990b92746760 100644 --- a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/Trigger.java +++ b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/Trigger.java @@ -12,6 +12,7 @@ public interface Trigger { /** * * @return the identifier + * @see Scheduled#identity() */ String getId(); diff --git a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java index b82838afa8878..1fbc8065f0b15 100644 --- a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java +++ b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java @@ -19,6 +19,7 @@ import javax.annotation.Priority; import javax.enterprise.event.Event; import javax.enterprise.event.Observes; +import javax.enterprise.inject.Any; import javax.enterprise.inject.Typed; import javax.inject.Singleton; import javax.interceptor.Interceptor; @@ -32,6 +33,7 @@ import com.cronutils.model.time.ExecutionTime; import com.cronutils.parser.CronParser; +import io.quarkus.arc.Arc; import io.quarkus.runtime.StartupEvent; import io.quarkus.scheduler.Scheduled; import io.quarkus.scheduler.Scheduled.ConcurrentExecution; @@ -91,6 +93,11 @@ public void run() { if (scheduled.concurrentExecution() == ConcurrentExecution.SKIP) { invoker = new SkipConcurrentExecutionInvoker(invoker, skippedExecutionEvent); } + if (!scheduled.skipExecutionIf().equals(Scheduled.Never.class)) { + invoker = new SkipPredicateInvoker(invoker, + Arc.container().select(scheduled.skipExecutionIf(), Any.Literal.INSTANCE).get(), + skippedExecutionEvent); + } scheduledTasks.add(new ScheduledTask(trigger.get(), invoker)); } } @@ -270,7 +277,7 @@ static abstract class SimpleTrigger implements Trigger { protected final ZonedDateTime start; protected volatile ZonedDateTime lastFireTime; - public SimpleTrigger(String id, ZonedDateTime start) { + SimpleTrigger(String id, ZonedDateTime start) { this.id = id; this.start = start; this.running = true; @@ -302,7 +309,7 @@ static class IntervalTrigger extends SimpleTrigger { // milliseconds private final long interval; - public IntervalTrigger(String id, ZonedDateTime start, long interval) { + IntervalTrigger(String id, ZonedDateTime start, long interval) { super(id, start); this.interval = interval; } @@ -351,7 +358,7 @@ static class CronTrigger extends SimpleTrigger { private final Cron cron; private final ExecutionTime executionTime; - public CronTrigger(String id, ZonedDateTime start, Cron cron) { + CronTrigger(String id, ZonedDateTime start, Cron cron) { super(id, start); this.cron = cron; this.executionTime = ExecutionTime.forCron(cron); diff --git a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SkipConcurrentExecutionInvoker.java b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SkipConcurrentExecutionInvoker.java index 26b3658b47311..21377ed01214e 100644 --- a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SkipConcurrentExecutionInvoker.java +++ b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SkipConcurrentExecutionInvoker.java @@ -40,7 +40,8 @@ public void invoke(ScheduledExecution execution) throws Exception { } } else { LOGGER.debugf("Skipped scheduled invoker execution: %s", delegate.getClass().getName()); - SkippedExecution payload = new SkippedExecution(execution.getTrigger().getId(), execution.getFireTime()); + SkippedExecution payload = new SkippedExecution(execution, + "The scheduled method should not be executed concurrently"); event.fire(payload); event.fireAsync(payload); } diff --git a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SkipPredicateInvoker.java b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SkipPredicateInvoker.java new file mode 100644 index 0000000000000..c0e87f320e8e4 --- /dev/null +++ b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SkipPredicateInvoker.java @@ -0,0 +1,49 @@ +package io.quarkus.scheduler.runtime; + +import javax.enterprise.event.Event; + +import org.jboss.logging.Logger; + +import io.quarkus.scheduler.Scheduled; +import io.quarkus.scheduler.ScheduledExecution; +import io.quarkus.scheduler.SkippedExecution; + +/** + * A scheduled invoker wrapper that skips the execution if the predicate evaluates to true. + * + * @see Scheduled#skipExecutionIf() + */ +public final class SkipPredicateInvoker implements ScheduledInvoker { + + private static final Logger LOGGER = Logger.getLogger(SkipPredicateInvoker.class); + + private final ScheduledInvoker delegate; + private final Scheduled.SkipPredicate predicate; + private final Event event; + + public SkipPredicateInvoker(ScheduledInvoker delegate, Scheduled.SkipPredicate predicate, + Event event) { + this.delegate = delegate; + this.predicate = predicate; + this.event = event; + } + + @Override + public void invoke(ScheduledExecution execution) throws Exception { + if (predicate.test(execution)) { + LOGGER.debugf("Skipped scheduled invoker execution: %s", delegate.getClass().getName()); + SkippedExecution payload = new SkippedExecution(execution, + predicate.getClass().getName()); + event.fire(payload); + event.fireAsync(payload); + } else { + delegate.invoke(execution); + } + } + + @Override + public void invokeBean(ScheduledExecution param) { + throw new UnsupportedOperationException(); + } + +}