diff --git a/docs/src/main/asciidoc/scheduler-reference.adoc b/docs/src/main/asciidoc/scheduler-reference.adoc index 70ac9a211ba900..9b3164a3bddf95 100644 --- a/docs/src/main/asciidoc/scheduler-reference.adoc +++ b/docs/src/main/asciidoc/scheduler-reference.adoc @@ -144,6 +144,8 @@ So for example, `15m` can be used instead of `PT15M` and is parsed as "15 minute void every15Mins() { } ---- +WARNING: A value less than one second may not be supported by the underlying scheduler implementation. In that case a warning message is logged during build and application start. + The `every` attribute supports <> including default values and nested Property Expressions. (Note that `"{property.path}"` style expressions are still supported but don't offer the full functionality of Property Expressions.) diff --git a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduled.java b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduled.java index 2fe195219ad13b..6a1e12c82e70e7 100644 --- a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduled.java +++ b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduled.java @@ -88,12 +88,15 @@ String cron() default ""; /** - * Defines a period between invocations. + * Defines the period between invocations. *

* The value is parsed with {@link Duration#parse(CharSequence)}. However, if an expression starts with a digit, "PT" prefix * is added automatically, so for example, {@code 15m} can be used instead of {@code PT15M} and is parsed as "15 minutes". * Note that the absolute value of the value is always used. *

+ * A value less than one second may not be supported by the underlying scheduler implementation. In that case a warning + * message is logged during build and application start. + *

* The value can be a property expression. In this case, the scheduler attempts to use the configured value instead: * {@code @Scheduled(every = "${myJob.everyExpression}")}. * Additionally, the property expression can specify a default value: {@code @Scheduled(every = diff --git a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/ScheduledExecution.java b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/ScheduledExecution.java index 4dd7d401f5f943..5f69240af667cf 100644 --- a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/ScheduledExecution.java +++ b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/ScheduledExecution.java @@ -3,19 +3,20 @@ import java.time.Instant; /** - * Scheduled execution metadata. + * Execution metadata of a specific scheduled job. */ public interface ScheduledExecution { /** * - * @return the trigger that fired the event + * @return the trigger that fired the execution */ Trigger getTrigger(); /** + * Unlike {@link Trigger#getPreviousFireTime()} this method always returns the same value. * - * @return the time the event was fired + * @return the time the associated trigger was fired */ Instant getFireTime(); diff --git a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduler.java b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduler.java index 86a506b94d1086..a6f2fffde045d6 100644 --- a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduler.java +++ b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduler.java @@ -111,6 +111,9 @@ interface JobDefinition { * The schedule is defined either by {@link #setCron(String)} or by {@link #setInterval(String)}. If both methods are * used, then the cron expression takes precedence. *

+ * A value less than one second may not be supported by the underlying scheduler implementation. In that case a warning + * message is logged immediately. + *

* {@link Scheduled#every()} * * @param every diff --git a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Trigger.java b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Trigger.java index f8d2037b40e2f9..c076e5712bc0eb 100644 --- a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Trigger.java +++ b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Trigger.java @@ -3,7 +3,10 @@ import java.time.Instant; /** - * Trigger is bound to a scheduled task. + * Trigger is bound to a scheduled job. + *

+ * It represents the logic that is used to test if a scheduled job should be executed + * at a specific time, i.e. the trigger is "fired". * * @see Scheduled */ @@ -11,8 +14,9 @@ public interface Trigger { /** * - * @return the identifier + * @return the identifier of the job * @see Scheduled#identity() + * @see Scheduler#newJob(String) */ String getId(); diff --git a/extensions/scheduler/deployment/pom.xml b/extensions/scheduler/deployment/pom.xml index 8271a16616d4ac..8082201f010c5a 100644 --- a/extensions/scheduler/deployment/pom.xml +++ b/extensions/scheduler/deployment/pom.xml @@ -50,6 +50,11 @@ quarkus-junit5-internal test + + org.assertj + assertj-core + test + io.rest-assured rest-assured 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 761598d93a6bcb..3b3e2e5f03b3ec 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 @@ -187,7 +187,8 @@ private void collectScheduledMethods(IndexView index, TransformedAnnotationsBuil @BuildStep void validateScheduledBusinessMethods(SchedulerConfig config, List scheduledMethods, - ValidationPhaseBuildItem validationPhase, BuildProducer validationErrors) { + ValidationPhaseBuildItem validationPhase, BuildProducer validationErrors, + Capabilities capabilities) { List errors = new ArrayList<>(); Map encounteredIdentities = new HashMap<>(); Set methodDescriptions = new HashSet<>(); @@ -240,9 +241,11 @@ void validateScheduledBusinessMethods(SchedulerConfig config, List encounteredIdentities, - BeanDeploymentValidator.ValidationContext validationContext) { + BeanDeploymentValidator.ValidationContext validationContext, long checkPeriod) { MethodInfo method = schedule.target().asMethod(); AnnotationValue cronValue = schedule.value("cron"); AnnotationValue everyValue = schedule.value("every"); @@ -519,7 +522,7 @@ private Throwable validateScheduled(CronParser parser, AnnotationInstance schedu try { parser.parse(cron).validate(); } catch (IllegalArgumentException e) { - return new IllegalStateException("Invalid cron() expression on: " + schedule, e); + return new IllegalStateException(errorMessage("Invalid cron() expression", schedule, method), e); } if (everyValue != null && !everyValue.asString().trim().isEmpty()) { LOGGER.warnf( @@ -535,7 +538,7 @@ private Throwable validateScheduled(CronParser parser, AnnotationInstance schedu try { ZoneId.of(timeZone); } catch (Exception e) { - return new IllegalStateException("Invalid timeZone() on " + schedule, e); + return new IllegalStateException(errorMessage("Invalid timeZone()", schedule, method), e); } } } @@ -548,9 +551,14 @@ private Throwable validateScheduled(CronParser parser, AnnotationInstance schedu every = "PT" + every; } try { - Duration.parse(every); + Duration period = Duration.parse(every); + if (period.toMillis() < checkPeriod) { + LOGGER.warnf( + "An every() value less than %s ms is not supported - the scheduled job will be executed with a delay: %s declared on %s#%s()", + checkPeriod, schedule, method.declaringClass().name(), method.name()); + } } catch (Exception e) { - return new IllegalStateException("Invalid every() expression on: " + schedule, e); + return new IllegalStateException(errorMessage("Invalid every() expression", schedule, method), e); } } } else { @@ -569,7 +577,7 @@ private Throwable validateScheduled(CronParser parser, AnnotationInstance schedu try { Duration.parse(delayed); } catch (Exception e) { - return new IllegalStateException("Invalid delayed() expression on: " + schedule, e); + return new IllegalStateException(errorMessage("Invalid delayed() expression", schedule, method), e); } } @@ -609,6 +617,10 @@ private Throwable validateScheduled(CronParser parser, AnnotationInstance schedu return null; } + private static String errorMessage(String base, AnnotationInstance scheduled, MethodInfo method) { + return String.format("%s: %s declared on %s#%s()", base, scheduled, method.declaringClass().name(), method.name()); + } + @BuildStep UnremovableBeanBuildItem unremoveableSkipPredicates() { return new UnremovableBeanBuildItem(new UnremovableBeanBuildItem.BeanTypeExclusion(SchedulerDotNames.SKIP_PREDICATE)); diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidCronExpressionTest.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidCronExpressionTest.java index 1b0b1beaae199a..fdfde8e4688511 100644 --- a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidCronExpressionTest.java +++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidCronExpressionTest.java @@ -1,6 +1,6 @@ package io.quarkus.scheduler.test; -import jakarta.enterprise.inject.spi.DeploymentException; +import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -12,7 +12,10 @@ public class InvalidCronExpressionTest { @RegisterExtension static final QuarkusUnitTest test = new QuarkusUnitTest() - .setExpectedException(DeploymentException.class) + .assertException(t -> { + assertThat(t).cause().isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Invalid cron() expression"); + }) .withApplicationRoot((jar) -> jar .addClasses(InvalidBean.class)); diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidDelayedExpressionTest.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidDelayedExpressionTest.java index de243471726e96..c348e9789336e1 100644 --- a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidDelayedExpressionTest.java +++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidDelayedExpressionTest.java @@ -1,6 +1,6 @@ package io.quarkus.scheduler.test; -import jakarta.enterprise.inject.spi.DeploymentException; +import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -12,7 +12,10 @@ public class InvalidDelayedExpressionTest { @RegisterExtension static final QuarkusUnitTest test = new QuarkusUnitTest() - .setExpectedException(DeploymentException.class) + .assertException(t -> { + assertThat(t).cause().isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Invalid delayed() expression"); + }) .withApplicationRoot((jar) -> jar .addClasses(InvalidBean.class)); diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidEveryExpressionTest.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidEveryExpressionTest.java index 56cbc8dffa64f8..332e9dd4db59d0 100644 --- a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidEveryExpressionTest.java +++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidEveryExpressionTest.java @@ -1,6 +1,6 @@ package io.quarkus.scheduler.test; -import jakarta.enterprise.inject.spi.DeploymentException; +import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -12,7 +12,10 @@ public class InvalidEveryExpressionTest { @RegisterExtension static final QuarkusUnitTest test = new QuarkusUnitTest() - .setExpectedException(DeploymentException.class) + .assertException(t -> { + assertThat(t).cause().isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Invalid every() expression"); + }) .withApplicationRoot((jar) -> jar .addClasses(InvalidEveryExpressionTest.InvalidBean.class)); diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidTimeZoneTest.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidTimeZoneTest.java index fa6c98bcbd4373..1e059125cd18b0 100644 --- a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidTimeZoneTest.java +++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidTimeZoneTest.java @@ -1,6 +1,6 @@ package io.quarkus.scheduler.test; -import jakarta.enterprise.inject.spi.DeploymentException; +import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -12,7 +12,9 @@ public class InvalidTimeZoneTest { @RegisterExtension static final QuarkusUnitTest test = new QuarkusUnitTest() - .setExpectedException(DeploymentException.class) + .assertException(t -> { + assertThat(t).cause().isInstanceOf(IllegalStateException.class).hasMessageContaining("Invalid timeZone()"); + }) .withApplicationRoot((jar) -> jar .addClasses(InvalidBean.class)); 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 afc99c574ee8d3..2a493c5d9170f9 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 @@ -81,7 +81,7 @@ public class SimpleScheduler implements Scheduler { private static final Logger LOG = Logger.getLogger(SimpleScheduler.class); // milliseconds - private static final long CHECK_PERIOD = 1000L; + public static final long CHECK_PERIOD = 1000L; private final ScheduledExecutorService scheduledExecutor; private final Vertx vertx; @@ -351,7 +351,7 @@ Optional createTrigger(String id, String methodDescription, CronP SchedulerUtils.parseCronTimeZone(scheduled), methodDescription)); } else if (!scheduled.every().isEmpty()) { final OptionalLong everyMillis = SchedulerUtils.parseEveryAsMillis(scheduled); - if (!everyMillis.isPresent()) { + if (everyMillis.isEmpty()) { return Optional.empty(); } return Optional.of(new IntervalTrigger(id, start, everyMillis.getAsLong(), @@ -505,6 +505,11 @@ static class IntervalTrigger extends SimpleTrigger { super(id, start, description); this.interval = interval; this.gracePeriod = gracePeriod; + if (interval < CHECK_PERIOD) { + LOG.warnf( + "An every() value less than %s ms is not supported - the scheduled job will be executed with a delay: %s", + CHECK_PERIOD, description); + } } @Override