From cefafe8d374d5fb7c1fddf0ac0edf7dd6b44a7f7 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Wed, 21 Sep 2022 12:19:18 +0300 Subject: [PATCH] Use a timer when awaiting Quartz shutdown The Quartz scheduler shutdown seems like it can block indefinitely if there is an exception in a yet to be completed job. To guard against this, we ensure that we only wait a configurable amount of time for the scheduler to shut down. This is a breaking change because previously we waited indefinitely. Relates to #28114 --- .../util/IsKotlinDataClassPredicate.java | 49 +++++++++++++++++++ .../quartz/runtime/QuartzBuildTimeConfig.java | 8 +++ .../quartz/runtime/QuartzScheduler.java | 26 ++++++++-- 3 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/util/IsKotlinDataClassPredicate.java diff --git a/core/deployment/src/main/java/io/quarkus/deployment/util/IsKotlinDataClassPredicate.java b/core/deployment/src/main/java/io/quarkus/deployment/util/IsKotlinDataClassPredicate.java new file mode 100644 index 0000000000000..24df5b2dafbc7 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/util/IsKotlinDataClassPredicate.java @@ -0,0 +1,49 @@ +package io.quarkus.deployment.util; + +import java.lang.reflect.Modifier; +import java.util.List; +import java.util.function.Predicate; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.MethodInfo; + +/** + * Tests whether a class is a data class (based on this answer: + * https://discuss.kotlinlang.org/t/detect-data-class-in-runtime/6155/2) + * and whether the class has default values for fields (default values leads to having multiple constructors in bytecode) + */ +public class IsKotlinDataClassPredicate implements Predicate { + + private final boolean testForDefaultValues; + + public IsKotlinDataClassPredicate(boolean testForDefaultValues) { + this.testForDefaultValues = testForDefaultValues; + } + + @Override + public boolean test(ClassInfo classInfo) { + int ctorCount = 0; + boolean hasCopyMethod = false; + boolean hasStaticCopyMethod = false; + boolean hasComponent1Method = false; + List methods = classInfo.methods(); + for (MethodInfo method : methods) { + String methodName = method.name(); + if ("".equals(methodName)) { + ctorCount++; + } else if ("component1".equals(methodName) && Modifier.isFinal(method.flags())) { + hasComponent1Method = true; + } else if ("copy".equals(methodName) && Modifier.isFinal(method.flags())) { + hasCopyMethod = true; + } else if ("copy$default".equals(methodName) && Modifier.isStatic(method.flags())) { + hasStaticCopyMethod = true; + } + } + + boolean isDataClass = hasComponent1Method && hasCopyMethod && hasStaticCopyMethod; + if (testForDefaultValues) { + return ctorCount > 1 && isDataClass; + } else { + return isDataClass; + } + } +} diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzBuildTimeConfig.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzBuildTimeConfig.java index f6399895ba720..cd643230693ff 100644 --- a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzBuildTimeConfig.java +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzBuildTimeConfig.java @@ -1,5 +1,6 @@ package io.quarkus.quartz.runtime; +import java.time.Duration; import java.util.Map; import java.util.Optional; @@ -57,6 +58,13 @@ public class QuartzBuildTimeConfig { @ConfigItem(defaultValue = "QRTZ_") public String tablePrefix; + /** + * The maximum amount of time Quarkus will wait for currently running jobs to finish. + * If the value is {@code 0}, then Quarkus will not wait at all for these jobs. + */ + @ConfigItem(defaultValue = "PT10S") + public Duration shutdownWaitTime; + /** * The SQL string that selects a row in the "LOCKS" table and places a lock on the row. *

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 17812724b7dae..3ce08a897829f 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 @@ -11,7 +11,10 @@ import java.util.Objects; import java.util.OptionalLong; import java.util.Properties; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; import java.util.function.Function; +import java.util.function.Supplier; import java.util.stream.Collectors; import javax.annotation.PreDestroy; @@ -89,6 +92,7 @@ public class QuartzScheduler implements Scheduler { private final org.quartz.Scheduler scheduler; private final boolean enabled; private final boolean startHalted; + private final Duration shutdownWaitTime; private final Map scheduledTasks = new HashMap<>(); public QuartzScheduler(SchedulerContext context, QuartzSupport quartzSupport, SchedulerRuntimeConfig schedulerRuntimeConfig, @@ -97,6 +101,7 @@ public QuartzScheduler(SchedulerContext context, QuartzSupport quartzSupport, Sc Vertx vertx) { enabled = schedulerRuntimeConfig.enabled; final Duration defaultOverdueGracePeriod = schedulerRuntimeConfig.overdueGracePeriod; + shutdownWaitTime = quartzSupport.getBuildTimeConfig().shutdownWaitTime; final QuartzRuntimeConfig runtimeConfig = quartzSupport.getRuntimeConfig(); boolean forceStart; @@ -478,9 +483,24 @@ void start(@Observes @Priority(Interceptor.Priority.PLATFORM_BEFORE) StartupEven void destroy(@Observes @BeforeDestroyed(ApplicationScoped.class) Object event) { if (scheduler != null) { try { - // Note that this method does not return until all currently executing jobs have completed - scheduler.shutdown(true); - } catch (SchedulerException e) { + if (shutdownWaitTime.isZero()) { + scheduler.shutdown(false); + } else { + CompletableFuture.supplyAsync(new Supplier<>() { + @Override + public Void get() { + // Note that this method does not return until all currently executing jobs have completed + try { + scheduler.shutdown(true); + } catch (SchedulerException e) { + throw new RuntimeException(e); + } + return null; + } + }).get(shutdownWaitTime.toMillis(), TimeUnit.MILLISECONDS); + } + + } catch (Exception e) { LOGGER.warnf("Unable to gracefully shutdown the scheduler", e); } }