From e7a7a587813d6bc577467587b1838ef922d8e3ee Mon Sep 17 00:00:00 2001 From: Manyanda Chitimbo Date: Thu, 21 Nov 2019 15:44:46 +0100 Subject: [PATCH 1/2] feat(quartz): add clustered jobs support Fixes #3520 --- bom/deployment/pom.xml | 5 + ci-templates/stages.yml | 1 + extensions/agroal/deployment/pom.xml | 4 + extensions/agroal/pom.xml | 1 + extensions/agroal/spi/pom.xml | 23 ++ .../deployment/DataSourceDriverBuildItem.java | 0 .../DataSourceInitializedBuildItem.java | 0 extensions/quartz/deployment/pom.xml | 104 ++++---- .../QuartzJDBCDriverDialectBuildItem.java | 20 ++ .../quartz/deployment/QuartzProcessor.java | 127 ++++++++-- .../quartz/test/MissingDataSourceTest.java | 26 ++ ...upportedClusteredJobConfigurationTest.java | 28 +++ extensions/quartz/runtime/pom.xml | 133 +++++----- .../QuarkusQuartzConnectionPoolProvider.java | 78 ++++++ .../quartz/runtime/QuartzBuildTimeConfig.java | 41 +++ .../quartz/runtime/QuartzRecorder.java | 7 +- .../quartz/runtime/QuartzScheduler.java | 128 +++++++--- .../quarkus/quartz/runtime/QuartzSupport.java | 17 +- .../io/quarkus/quartz/runtime/StoreType.java | 17 ++ .../runtime/graal/QuartzSubstitutions.java | 28 +++ integration-tests/pom.xml | 1 + integration-tests/quartz/pom.xml | 131 ++++++++++ .../io/quarkus/it/quartz/CountResource.java | 20 ++ .../java/io/quarkus/it/quartz/Counter.java | 29 +++ .../src/main/resources/application.properties | 17 ++ .../db/migration/V1.0.1__QuarkusQuartz.sql | 238 ++++++++++++++++++ .../io/quarkus/it/quartz/QuartzITCase.java | 8 + .../io/quarkus/it/quartz/QuartzTestCase.java | 28 +++ .../io/quarkus/it/quartz/TestResources.java | 8 + 29 files changed, 1095 insertions(+), 173 deletions(-) create mode 100644 extensions/agroal/spi/pom.xml rename extensions/agroal/{deployment => spi}/src/main/java/io/quarkus/agroal/deployment/DataSourceDriverBuildItem.java (100%) rename extensions/agroal/{deployment => spi}/src/main/java/io/quarkus/agroal/deployment/DataSourceInitializedBuildItem.java (100%) create mode 100644 extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/QuartzJDBCDriverDialectBuildItem.java create mode 100644 extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/MissingDataSourceTest.java create mode 100644 extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/UnsupportedClusteredJobConfigurationTest.java create mode 100644 extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuarkusQuartzConnectionPoolProvider.java create mode 100644 extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzBuildTimeConfig.java create mode 100644 extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/StoreType.java create mode 100644 integration-tests/quartz/pom.xml create mode 100644 integration-tests/quartz/src/main/java/io/quarkus/it/quartz/CountResource.java create mode 100644 integration-tests/quartz/src/main/java/io/quarkus/it/quartz/Counter.java create mode 100644 integration-tests/quartz/src/main/resources/application.properties create mode 100644 integration-tests/quartz/src/main/resources/db/migration/V1.0.1__QuarkusQuartz.sql create mode 100644 integration-tests/quartz/src/test/java/io/quarkus/it/quartz/QuartzITCase.java create mode 100644 integration-tests/quartz/src/test/java/io/quarkus/it/quartz/QuartzTestCase.java create mode 100644 integration-tests/quartz/src/test/java/io/quarkus/it/quartz/TestResources.java diff --git a/bom/deployment/pom.xml b/bom/deployment/pom.xml index d971194fab1a3..d68c2b2084011 100644 --- a/bom/deployment/pom.xml +++ b/bom/deployment/pom.xml @@ -106,6 +106,11 @@ quarkus-agroal-deployment ${project.version} + + io.quarkus + quarkus-agroal-spi + ${project.version} + io.quarkus quarkus-artemis-core diff --git a/ci-templates/stages.yml b/ci-templates/stages.yml index e2f47e4bb51f5..07c1e930bc4a7 100644 --- a/ci-templates/stages.yml +++ b/ci-templates/stages.yml @@ -348,6 +348,7 @@ stages: modules: - kogito - kubernetes-client + - quartz name: misc_3 - template: native-build-steps.yaml diff --git a/extensions/agroal/deployment/pom.xml b/extensions/agroal/deployment/pom.xml index 03167b34a1efd..92d20fa9b228a 100644 --- a/extensions/agroal/deployment/pom.xml +++ b/extensions/agroal/deployment/pom.xml @@ -26,6 +26,10 @@ io.quarkus quarkus-agroal + + io.quarkus + quarkus-agroal-spi + io.quarkus quarkus-narayana-jta-deployment diff --git a/extensions/agroal/pom.xml b/extensions/agroal/pom.xml index 47b4f6984e30d..4eb57e49b3eb2 100644 --- a/extensions/agroal/pom.xml +++ b/extensions/agroal/pom.xml @@ -14,6 +14,7 @@ Quarkus - Agroal pom + spi deployment runtime diff --git a/extensions/agroal/spi/pom.xml b/extensions/agroal/spi/pom.xml new file mode 100644 index 0000000000000..dca7328ae8c8b --- /dev/null +++ b/extensions/agroal/spi/pom.xml @@ -0,0 +1,23 @@ + + + + quarkus-agroal-parent + io.quarkus + 999-SNAPSHOT + ../ + + 4.0.0 + + quarkus-agroal-spi + Quarkus - Agroal - SPI + + + + io.quarkus + quarkus-core-deployment + + + + diff --git a/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/DataSourceDriverBuildItem.java b/extensions/agroal/spi/src/main/java/io/quarkus/agroal/deployment/DataSourceDriverBuildItem.java similarity index 100% rename from extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/DataSourceDriverBuildItem.java rename to extensions/agroal/spi/src/main/java/io/quarkus/agroal/deployment/DataSourceDriverBuildItem.java diff --git a/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/DataSourceInitializedBuildItem.java b/extensions/agroal/spi/src/main/java/io/quarkus/agroal/deployment/DataSourceInitializedBuildItem.java similarity index 100% rename from extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/DataSourceInitializedBuildItem.java rename to extensions/agroal/spi/src/main/java/io/quarkus/agroal/deployment/DataSourceInitializedBuildItem.java diff --git a/extensions/quartz/deployment/pom.xml b/extensions/quartz/deployment/pom.xml index 5e0e82e4636f5..ce55f3ac36e3b 100644 --- a/extensions/quartz/deployment/pom.xml +++ b/extensions/quartz/deployment/pom.xml @@ -1,58 +1,62 @@ - - quarkus-quartz-parent - io.quarkus - 999-SNAPSHOT - ../ - + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + + quarkus-quartz-parent + io.quarkus + 999-SNAPSHOT + ../ + - 4.0.0 + 4.0.0 - quarkus-quartz-deployment - Quarkus - Scheduler Quartz - Deployment + quarkus-quartz-deployment + Quarkus - Quartz - Deployment - - - io.quarkus - quarkus-core-deployment - - - io.quarkus - quarkus-arc-deployment - - - io.quarkus - quarkus-scheduler-deployment - - - io.quarkus - quarkus-quartz - + + + io.quarkus + quarkus-core-deployment + + + io.quarkus + quarkus-arc-deployment + + + io.quarkus + quarkus-scheduler-deployment + + + io.quarkus + quarkus-agroal-spi + + + io.quarkus + quarkus-quartz + - - io.quarkus - quarkus-junit5-internal - test - - + + io.quarkus + quarkus-junit5-internal + test + + - - - - maven-compiler-plugin - - - - io.quarkus - quarkus-extension-processor - ${project.version} - - - - - - + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + diff --git a/extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/QuartzJDBCDriverDialectBuildItem.java b/extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/QuartzJDBCDriverDialectBuildItem.java new file mode 100644 index 0000000000000..0fa3375a54c19 --- /dev/null +++ b/extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/QuartzJDBCDriverDialectBuildItem.java @@ -0,0 +1,20 @@ +package io.quarkus.quartz.deployment; + +import java.util.Optional; + +import io.quarkus.builder.item.SimpleBuildItem; + +/** + * Holds the SQL driver dialect {@link org.quartz.impl.jdbcjobstore.StdJDBCDelegate driver delegate} to use. + */ +final class QuartzJDBCDriverDialectBuildItem extends SimpleBuildItem { + private final Optional driver; + + public QuartzJDBCDriverDialectBuildItem(Optional driver) { + this.driver = driver; + } + + public Optional getDriver() { + return driver; + } +} diff --git a/extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/QuartzProcessor.java b/extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/QuartzProcessor.java index 220de421ecee0..9c8f66702c9cf 100644 --- a/extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/QuartzProcessor.java +++ b/extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/QuartzProcessor.java @@ -2,13 +2,27 @@ import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT; +import java.sql.Connection; import java.util.ArrayList; import java.util.List; +import java.util.Optional; +import org.quartz.core.QuartzSchedulerThread; +import org.quartz.core.SchedulerSignalerImpl; +import org.quartz.impl.StdSchedulerFactory; +import org.quartz.impl.jdbcjobstore.AttributeRestoringConnectionInvocationHandler; +import org.quartz.impl.jdbcjobstore.HSQLDBDelegate; +import org.quartz.impl.jdbcjobstore.JobStoreSupport; +import org.quartz.impl.jdbcjobstore.MSSQLDelegate; +import org.quartz.impl.jdbcjobstore.PostgreSQLDelegate; +import org.quartz.impl.jdbcjobstore.StdJDBCDelegate; +import org.quartz.impl.triggers.AbstractTrigger; +import org.quartz.impl.triggers.SimpleTriggerImpl; import org.quartz.simpl.CascadingClassLoadHelper; -import org.quartz.simpl.RAMJobStore; +import org.quartz.simpl.SimpleInstanceIdGenerator; import org.quartz.simpl.SimpleThreadPool; +import io.quarkus.agroal.deployment.DataSourceDriverBuildItem; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.BeanContainerBuildItem; import io.quarkus.deployment.Capabilities; @@ -17,18 +31,22 @@ import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.CapabilityBuildItem; import io.quarkus.deployment.builditem.ServiceStartBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageProxyDefinitionBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.deployment.configuration.ConfigurationError; import io.quarkus.deployment.logging.LogCleanupFilterBuildItem; +import io.quarkus.quartz.runtime.QuarkusQuartzConnectionPoolProvider; +import io.quarkus.quartz.runtime.QuartzBuildTimeConfig; import io.quarkus.quartz.runtime.QuartzRecorder; import io.quarkus.quartz.runtime.QuartzRuntimeConfig; import io.quarkus.quartz.runtime.QuartzScheduler; import io.quarkus.quartz.runtime.QuartzSupport; +import io.quarkus.quartz.runtime.StoreType; /** * @author Martin Kouba */ public class QuartzProcessor { - @BuildStep CapabilityBuildItem capability() { return new CapabilityBuildItem(Capabilities.QUARTZ); @@ -40,41 +58,118 @@ AdditionalBeanBuildItem beans() { } @BuildStep - List reflectiveClasses() { + NativeImageProxyDefinitionBuildItem connectionProxy(QuartzBuildTimeConfig config) { + if (config.storeType.equals(StoreType.DB)) { + return new NativeImageProxyDefinitionBuildItem(Connection.class.getName()); + } + return null; + } + + @BuildStep + QuartzJDBCDriverDialectBuildItem driver(Optional dataSourceDriver, + QuartzBuildTimeConfig config) { + if (config.storeType == StoreType.RAM) { + if (config.clustered) { + throw new ConfigurationError("Clustered jobs configured with unsupported job store option"); + } + + return new QuartzJDBCDriverDialectBuildItem(Optional.empty()); + } + + if (!dataSourceDriver.isPresent()) { + String message = String.format( + "JDBC Store configured but '%s' datasource is not configured properly. You can configure your datasource by following the guide available at: https://quarkus.io/guides/datasource-guide", + config.dataSourceName.isPresent() ? config.dataSourceName.get() : "default"); + throw new ConfigurationError(message); + } + + return new QuartzJDBCDriverDialectBuildItem(Optional.of(guessDriver(dataSourceDriver))); + } + + private String guessDriver(Optional dataSourceDriver) { + if (!dataSourceDriver.isPresent()) { + return StdJDBCDelegate.class.getName(); + } + + String resolvedDriver = dataSourceDriver.get().getDriver(); + if (resolvedDriver.contains("postgresql")) { + return PostgreSQLDelegate.class.getName(); + } + if (resolvedDriver.contains("org.h2.Driver")) { + return HSQLDBDelegate.class.getName(); + } + + if (resolvedDriver.contains("com.microsoft.sqlserver.jdbc.SQLServerResource")) { + return MSSQLDelegate.class.getName(); + } + + return StdJDBCDelegate.class.getName(); + + } + + @BuildStep + List reflectiveClasses(QuartzBuildTimeConfig config, + QuartzJDBCDriverDialectBuildItem driverDialect) { List reflectiveClasses = new ArrayList<>(); - reflectiveClasses.add(new ReflectiveClassBuildItem(false, false, CascadingClassLoadHelper.class.getName())); + StoreType storeType = config.storeType; + reflectiveClasses.add(new ReflectiveClassBuildItem(true, false, SimpleThreadPool.class.getName())); - reflectiveClasses.add(new ReflectiveClassBuildItem(true, false, RAMJobStore.class.getName())); + reflectiveClasses.add(new ReflectiveClassBuildItem(true, false, SimpleInstanceIdGenerator.class.getName())); + reflectiveClasses.add(new ReflectiveClassBuildItem(false, false, CascadingClassLoadHelper.class.getName())); + reflectiveClasses.add(new ReflectiveClassBuildItem(true, true, storeType.clazz)); + + if (storeType.equals(StoreType.DB)) { + reflectiveClasses.add(new ReflectiveClassBuildItem(true, false, JobStoreSupport.class.getName())); + reflectiveClasses.add(new ReflectiveClassBuildItem(true, true, Connection.class.getName())); + reflectiveClasses.add(new ReflectiveClassBuildItem(true, false, AbstractTrigger.class.getName())); + reflectiveClasses.add(new ReflectiveClassBuildItem(true, false, SimpleTriggerImpl.class.getName())); + reflectiveClasses.add(new ReflectiveClassBuildItem(true, false, driverDialect.getDriver().get())); + reflectiveClasses + .add(new ReflectiveClassBuildItem(true, true, "io.quarkus.quartz.runtime.QuartzScheduler$InvokerJob")); + reflectiveClasses + .add(new ReflectiveClassBuildItem(true, false, QuarkusQuartzConnectionPoolProvider.class.getName())); + } + return reflectiveClasses; } @BuildStep - public void logCleanup(BuildProducer logCleanupFilter) { - logCleanupFilter.produce(new LogCleanupFilterBuildItem("org.quartz.impl.StdSchedulerFactory", + public List logCleanup(QuartzBuildTimeConfig config) { + StoreType storeType = config.storeType; + List logCleanUps = new ArrayList<>(); + logCleanUps.add(new LogCleanupFilterBuildItem(StdSchedulerFactory.class.getName(), "Quartz scheduler version:", "Using default implementation for", "Quartz scheduler 'QuarkusQuartzScheduler'")); - logCleanupFilter.produce(new LogCleanupFilterBuildItem("org.quartz.core.QuartzScheduler", + logCleanUps.add(new LogCleanupFilterBuildItem(org.quartz.core.QuartzScheduler.class.getName(), "Quartz Scheduler v", "JobFactory set to:", "Scheduler meta-data:", "Scheduler QuarkusQuartzScheduler")); - logCleanupFilter.produce(new LogCleanupFilterBuildItem("org.quartz.simpl.RAMJobStore", - "RAMJobStore initialized.")); - - logCleanupFilter.produce(new LogCleanupFilterBuildItem("org.quartz.core.SchedulerSignalerImpl", + logCleanUps.add(new LogCleanupFilterBuildItem(storeType.clazz, storeType.name + " initialized.", "Handling", + "Using db table-based data access locking", "JDBCJobStore threads will inherit ContextClassLoader of thread", + "Couldn't rollback jdbc connection", "Database connection shutdown unsuccessful")); + logCleanUps.add(new LogCleanupFilterBuildItem(SchedulerSignalerImpl.class.getName(), "Initialized Scheduler Signaller of type")); + logCleanUps.add(new LogCleanupFilterBuildItem(QuartzSchedulerThread.class.getName(), + "QuartzSchedulerThread Inheriting ContextClassLoader")); + logCleanUps.add(new LogCleanupFilterBuildItem(SimpleThreadPool.class.getName(), + "Job execution threads will use class loader of thread")); + + logCleanUps.add(new LogCleanupFilterBuildItem(AttributeRestoringConnectionInvocationHandler.class.getName(), + "Failed restore connection's original")); + return logCleanUps; } @BuildStep @Record(RUNTIME_INIT) - public void build(QuartzRuntimeConfig runtimeConfig, QuartzRecorder recorder, BeanContainerBuildItem beanContainer, - BuildProducer serviceStart) { - recorder.initialize(runtimeConfig, beanContainer.getValue()); + public void build(QuartzRuntimeConfig runtimeConfig, QuartzBuildTimeConfig buildTimeConfig, QuartzRecorder recorder, + BeanContainerBuildItem beanContainer, + BuildProducer serviceStart, QuartzJDBCDriverDialectBuildItem driverDialect) { + recorder.initialize(runtimeConfig, buildTimeConfig, beanContainer.getValue(), driverDialect.getDriver()); // Make sure that StartupEvent is fired after the init serviceStart.produce(new ServiceStartBuildItem("quartz")); } - } diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/MissingDataSourceTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/MissingDataSourceTest.java new file mode 100644 index 0000000000000..04a1694bdfbc9 --- /dev/null +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/MissingDataSourceTest.java @@ -0,0 +1,26 @@ +package io.quarkus.quartz.test; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.deployment.configuration.ConfigurationError; +import io.quarkus.test.QuarkusUnitTest; + +public class MissingDataSourceTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .setExpectedException(ConfigurationError.class) + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(SimpleJobs.class) + .addAsResource(new StringAsset("quarkus.quartz.store-type=db"), "application.properties")); + + @Test + public void shouldFailAndNotReachHere() { + Assertions.fail(); + } +} diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/UnsupportedClusteredJobConfigurationTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/UnsupportedClusteredJobConfigurationTest.java new file mode 100644 index 0000000000000..0fc885b2ed857 --- /dev/null +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/UnsupportedClusteredJobConfigurationTest.java @@ -0,0 +1,28 @@ +package io.quarkus.quartz.test; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.deployment.configuration.ConfigurationError; +import io.quarkus.test.QuarkusUnitTest; + +public class UnsupportedClusteredJobConfigurationTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .setExpectedException(ConfigurationError.class) + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(SimpleJobs.class) + .addAsResource(new StringAsset( + "quarkus.quartz.store-type=ram\nquarkus.quartz.clustered=true"), + "application.properties")); + + @Test + public void shouldFailWhenConfiguringClusteredJobWithRamStore() { + Assertions.fail(); + } +} diff --git a/extensions/quartz/runtime/pom.xml b/extensions/quartz/runtime/pom.xml index cfc00d070b7b5..af327d6c85612 100644 --- a/extensions/quartz/runtime/pom.xml +++ b/extensions/quartz/runtime/pom.xml @@ -1,71 +1,76 @@ - - quarkus-quartz-parent - io.quarkus - 999-SNAPSHOT - ../ - - 4.0.0 + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + + quarkus-quartz-parent + io.quarkus + 999-SNAPSHOT + ../ + + 4.0.0 - quarkus-quartz - Quarkus - Scheduler Quartz - Runtime - - - io.quarkus - quarkus-scheduler - - - com.oracle.substratevm - svm - - - org.quartz-scheduler - quartz - - - com.zaxxer - HikariCP-java6 - - - com.mchange - c3p0 - - - - - jakarta.transaction - jakarta.transaction-api - - + quarkus-quartz + Quarkus - Quartz - Runtime + + + io.quarkus + quarkus-agroal + true + + + io.quarkus + quarkus-scheduler + + + com.oracle.substratevm + svm + + + org.quartz-scheduler + quartz + + + com.zaxxer + HikariCP-java6 + + + com.mchange + c3p0 + + + + + jakarta.transaction + jakarta.transaction-api + + - + - - - - io.quarkus - quarkus-bootstrap-maven-plugin - - - maven-compiler-plugin - - - - io.quarkus - quarkus-extension-processor - ${project.version} - - - - - - + + + + io.quarkus + quarkus-bootstrap-maven-plugin + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuarkusQuartzConnectionPoolProvider.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuarkusQuartzConnectionPoolProvider.java new file mode 100644 index 0000000000000..e39fd122baec5 --- /dev/null +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuarkusQuartzConnectionPoolProvider.java @@ -0,0 +1,78 @@ +package io.quarkus.quartz.runtime; + +import java.sql.Connection; +import java.sql.SQLException; + +import javax.enterprise.util.AnnotationLiteral; +import javax.sql.DataSource; + +import org.quartz.utils.PoolingConnectionProvider; + +import io.agroal.api.AgroalDataSource; +import io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; +import io.quarkus.arc.InstanceHandle; + +public class QuarkusQuartzConnectionPoolProvider implements PoolingConnectionProvider { + private AgroalDataSource dataSource; + private static String dataSourceName; + + public QuarkusQuartzConnectionPoolProvider() { + final ArcContainer container = Arc.container(); + final InstanceHandle instanceHandle; + final boolean useDefaultDataSource = "QUARKUS_QUARTZ_DEFAULT_DATASOURCE".equals(dataSourceName); + if (useDefaultDataSource) { + instanceHandle = container.instance(AgroalDataSource.class); + } else { + instanceHandle = container.instance(AgroalDataSource.class, new DataSourceLiteral(dataSourceName)); + } + if (instanceHandle.isAvailable()) { + this.dataSource = instanceHandle.get(); + } else { + String message = String.format( + "JDBC Store configured but '%s' datasource is missing. You can configure your datasource by following the guide available at: https://quarkus.io/guides/datasource", + useDefaultDataSource ? "default" : dataSourceName); + throw new IllegalStateException(message); + } + } + + @Override + public DataSource getDataSource() { + return dataSource; + } + + @Override + public Connection getConnection() throws SQLException { + return dataSource.getConnection(); + } + + @Override + public void shutdown() { + // Do nothing as the connection will be closed inside the Agroal extension + } + + @Override + public void initialize() { + + } + + static void setDataSourceName(String dataSourceName) { + QuarkusQuartzConnectionPoolProvider.dataSourceName = dataSourceName; + } + + private static class DataSourceLiteral extends AnnotationLiteral + implements io.quarkus.agroal.DataSource { + + private String name; + + public DataSourceLiteral(String name) { + this.name = name; + } + + @Override + public String value() { + return name; + } + + } +} 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 new file mode 100644 index 0000000000000..4411910f4a0f0 --- /dev/null +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzBuildTimeConfig.java @@ -0,0 +1,41 @@ +package io.quarkus.quartz.runtime; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED) +public class QuartzBuildTimeConfig { + /** + * Enable cluster mode or not. + *

+ * If enabled make sure to set the appropriate cluster properties. + */ + @ConfigItem + public boolean clustered; + + /** + * The type of store to use. + *

+ * When using the `db` store type configuration value make sure that you have the datasource configured. + * See Configuring your datasource for more information. + *

+ * To create Quartz tables, you can perform a schema migration via the Flyway + * extension using a SQL script matching your database picked from Quartz + * repository. + */ + @ConfigItem(defaultValue = "ram") + public StoreType storeType; + + /** + * The name of the datasource to use. + *

+ * Optionally needed when using the `db` store type. + * If not specified, defaults to using the default datasource. + */ + @ConfigItem(name = "datasource") + public Optional dataSourceName; +} diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzRecorder.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzRecorder.java index d694e8fe5ac1a..cbbd0b4892d2e 100644 --- a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzRecorder.java +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzRecorder.java @@ -1,14 +1,17 @@ package io.quarkus.quartz.runtime; +import java.util.Optional; + import io.quarkus.arc.runtime.BeanContainer; import io.quarkus.runtime.annotations.Recorder; @Recorder public class QuartzRecorder { - public void initialize(QuartzRuntimeConfig runtimeConfig, BeanContainer container) { + public void initialize(QuartzRuntimeConfig runTimeConfig, QuartzBuildTimeConfig buildTimeConfig, BeanContainer container, + Optional driverDialect) { QuartzSupport support = container.instance(QuartzSupport.class); - support.initialize(runtimeConfig); + support.initialize(runTimeConfig, buildTimeConfig, driverDialect); } } 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 1198bc60441a3..0ed976fce018f 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 @@ -9,6 +9,8 @@ import java.util.concurrent.atomic.AtomicInteger; import javax.annotation.PreDestroy; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.BeforeDestroyed; import javax.enterprise.event.Observes; import javax.inject.Singleton; @@ -17,8 +19,8 @@ import org.quartz.CronScheduleBuilder; import org.quartz.Job; import org.quartz.JobBuilder; +import org.quartz.JobDetail; import org.quartz.JobExecutionContext; -import org.quartz.JobExecutionException; import org.quartz.ScheduleBuilder; import org.quartz.SchedulerException; import org.quartz.SchedulerFactory; @@ -55,7 +57,6 @@ public class QuartzScheduler implements Scheduler { private final Map invokers; public QuartzScheduler(SchedulerSupport schedulerSupport, QuartzSupport quartzSupport, Config config) { - if (schedulerSupport.getScheduledMethods().isEmpty()) { this.triggerNameSequence = null; this.scheduler = null; @@ -66,21 +67,7 @@ public QuartzScheduler(SchedulerSupport schedulerSupport, QuartzSupport quartzSu this.invokers = new HashMap<>(); try { - Properties props = new Properties(); - props.put(StdSchedulerFactory.PROP_SCHED_INSTANCE_ID, "QuarkusQuartzScheduler"); - props.put(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME, "QuarkusQuartzScheduler"); - props.put(StdSchedulerFactory.PROP_SCHED_WRAP_JOB_IN_USER_TX, false); - props.put(StdSchedulerFactory.PROP_SCHED_SCHEDULER_THREADS_INHERIT_CONTEXT_CLASS_LOADER_OF_INITIALIZING_THREAD, - true); - props.put(StdSchedulerFactory.PROP_THREAD_POOL_CLASS, "org.quartz.simpl.SimpleThreadPool"); - props.put(StdSchedulerFactory.PROP_THREAD_POOL_PREFIX + ".threadCount", - "" + quartzSupport.getRuntimeConfig().threadCount); - props.put(StdSchedulerFactory.PROP_THREAD_POOL_PREFIX + ".threadPriority", - "" + quartzSupport.getRuntimeConfig().threadPriority); - props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".misfireThreshold", "60000"); - props.put(StdSchedulerFactory.PROP_JOB_STORE_CLASS, "org.quartz.simpl.RAMJobStore"); - props.put(StdSchedulerFactory.PROP_SCHED_RMI_EXPORT, false); - props.put(StdSchedulerFactory.PROP_SCHED_RMI_PROXY, false); + Properties props = getSchedulerConfigurationProperties(quartzSupport); SchedulerFactory schedulerFactory = new StdSchedulerFactory(props); scheduler = schedulerFactory.getScheduler(); @@ -109,8 +96,9 @@ public Job newJob(TriggerFiredBundle bundle, org.quartz.Scheduler scheduler) thr for (Scheduled scheduled : method.getSchedules()) { String name = triggerNameSequence.getAndIncrement() + "_" + method.getInvokerClassName(); JobBuilder jobBuilder = JobBuilder.newJob(InvokerJob.class) - .withIdentity(name, Scheduler.class.getName()).usingJobData(INVOKER_KEY, - method.getInvokerClassName()); + .withIdentity(name, Scheduler.class.getName()) + .usingJobData(INVOKER_KEY, method.getInvokerClassName()) + .requestRecovery(); ScheduleBuilder scheduleBuilder; String cron = scheduled.cron().trim(); @@ -161,8 +149,13 @@ public Job newJob(TriggerFiredBundle bundle, org.quartz.Scheduler scheduler) thr .plusMillis(scheduled.delayUnit().toMillis(scheduled.delay())).toEpochMilli())); } - scheduler.scheduleJob(jobBuilder.build(), triggerBuilder.build()); - LOGGER.debugf("Scheduled business method %s with config %s", method.getMethodDescription(), scheduled); + JobDetail job = jobBuilder.build(); + if (scheduler.checkExists(job.getKey())) { + scheduler.deleteJob(job.getKey()); + } + scheduler.scheduleJob(job, triggerBuilder.build()); + LOGGER.debugf("Scheduled business method %s with config %s", method.getMethodDescription(), + scheduled); } } } catch (SchedulerException e) { @@ -204,21 +197,76 @@ void start(@Observes StartupEvent startupEvent) { } } + /** + * Need to gracefully shutdown the scheduler making sure that all triggers have been + * released before datasource shutdown. + * + * @param event ignored + */ + void destroy(@BeforeDestroyed(ApplicationScoped.class) Object event) { // + if (scheduler != null) { + try { + scheduler.shutdown(true); // gracefully shutdown + } catch (SchedulerException e) { + LOGGER.warnf("Unable to gracefully shutdown the scheduler", e); + } + } + } + @PreDestroy void destroy() { if (scheduler != null) { try { - scheduler.shutdown(); + if (!scheduler.isShutdown()) { + scheduler.shutdown(false); // force shutdown + } } catch (SchedulerException e) { - LOGGER.warnf("Unable to shutdown scheduler", e); + LOGGER.warnf("Unable to shutdown the scheduler", e); } } } + private Properties getSchedulerConfigurationProperties(QuartzSupport quartzSupport) { + Properties props = new Properties(); + QuartzBuildTimeConfig buildTimeConfig = quartzSupport.getBuildTimeConfig(); + props.put(StdSchedulerFactory.PROP_SCHED_INSTANCE_ID, "AUTO"); + props.put("org.quartz.scheduler.skipUpdateCheck", "true"); + props.put(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME, "QuarkusQuartzScheduler"); + props.put(StdSchedulerFactory.PROP_SCHED_WRAP_JOB_IN_USER_TX, "false"); + props.put(StdSchedulerFactory.PROP_SCHED_SCHEDULER_THREADS_INHERIT_CONTEXT_CLASS_LOADER_OF_INITIALIZING_THREAD, "true"); + props.put(StdSchedulerFactory.PROP_THREAD_POOL_CLASS, "org.quartz.simpl.SimpleThreadPool"); + props.put(StdSchedulerFactory.PROP_THREAD_POOL_PREFIX + ".threadCount", + "" + quartzSupport.getRuntimeConfig().threadCount); + props.put(StdSchedulerFactory.PROP_THREAD_POOL_PREFIX + ".threadPriority", + "" + quartzSupport.getRuntimeConfig().threadPriority); + props.put(StdSchedulerFactory.PROP_SCHED_RMI_EXPORT, "false"); + props.put(StdSchedulerFactory.PROP_SCHED_RMI_PROXY, "false"); + props.put(StdSchedulerFactory.PROP_JOB_STORE_CLASS, buildTimeConfig.storeType.clazz); + + if (buildTimeConfig.storeType == StoreType.DB) { + String dataSource = buildTimeConfig.dataSourceName.orElse("QUARKUS_QUARTZ_DEFAULT_DATASOURCE"); + QuarkusQuartzConnectionPoolProvider.setDataSourceName(dataSource); + props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".useProperties", "true"); + props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".misfireThreshold", "60000"); + props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".tablePrefix", "QRTZ_"); + props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".dataSource", dataSource); + props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".driverDelegateClass", + quartzSupport.getDriverDialect().get()); + props.put(StdSchedulerFactory.PROP_DATASOURCE_PREFIX + "." + dataSource + ".connectionProvider.class", + QuarkusQuartzConnectionPoolProvider.class.getName()); + if (buildTimeConfig.clustered) { + props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".isClustered", "true"); + props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".clusterCheckinInterval", "20000"); // 20 seconds + } + } + + return props; + } + class InvokerJob implements Job { @Override - public void execute(JobExecutionContext context) throws JobExecutionException { + public void execute(JobExecutionContext context) { Trigger trigger = new Trigger() { @Override @@ -239,23 +287,25 @@ public String getId() { } }; String invokerClass = context.getJobDetail().getJobDataMap().getString(INVOKER_KEY); - invokers.get(invokerClass).invoke(new ScheduledExecution() { - - @Override - public Trigger getTrigger() { - return trigger; - } + ScheduledInvoker scheduledInvoker = invokers.get(invokerClass); + if (scheduledInvoker != null) { // could be null from previous runs + scheduledInvoker.invoke(new ScheduledExecution() { + @Override + public Trigger getTrigger() { + return trigger; + } - @Override - public Instant getScheduledFireTime() { - return context.getScheduledFireTime().toInstant(); - } + @Override + public Instant getScheduledFireTime() { + return context.getScheduledFireTime().toInstant(); + } - @Override - public Instant getFireTime() { - return context.getFireTime().toInstant(); - } - }); + @Override + public Instant getFireTime() { + return context.getFireTime().toInstant(); + } + }); + } } } diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSupport.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSupport.java index 53a908533ef6e..c510e585d4973 100644 --- a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSupport.java +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSupport.java @@ -1,18 +1,31 @@ package io.quarkus.quartz.runtime; +import java.util.Optional; + import javax.inject.Singleton; @Singleton public class QuartzSupport { private QuartzRuntimeConfig runtimeConfig; + private QuartzBuildTimeConfig buildTimeConfig; + private Optional driverDialect; - void initialize(QuartzRuntimeConfig runtimeConfig) { - this.runtimeConfig = runtimeConfig; + void initialize(QuartzRuntimeConfig runTimeConfig, QuartzBuildTimeConfig buildTimeConfig, Optional driverDialect) { + this.runtimeConfig = runTimeConfig; + this.buildTimeConfig = buildTimeConfig; + this.driverDialect = driverDialect; } public QuartzRuntimeConfig getRuntimeConfig() { return runtimeConfig; } + public QuartzBuildTimeConfig getBuildTimeConfig() { + return buildTimeConfig; + } + + public Optional getDriverDialect() { + return driverDialect; + } } diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/StoreType.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/StoreType.java new file mode 100644 index 0000000000000..fb43eebbc3289 --- /dev/null +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/StoreType.java @@ -0,0 +1,17 @@ +package io.quarkus.quartz.runtime; + +import org.quartz.impl.jdbcjobstore.JobStoreTX; +import org.quartz.simpl.RAMJobStore; + +public enum StoreType { + RAM(RAMJobStore.class.getName(), RAMJobStore.class.getSimpleName()), + DB(JobStoreTX.class.getName(), JobStoreTX.class.getSimpleName()); + + public String name; + public String clazz; + + StoreType(String clazz, String name) { + this.clazz = clazz; + this.name = name; + } +} diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/graal/QuartzSubstitutions.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/graal/QuartzSubstitutions.java index c408fb2f1fcd2..07cc9ced71ee5 100644 --- a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/graal/QuartzSubstitutions.java +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/graal/QuartzSubstitutions.java @@ -1,8 +1,11 @@ package io.quarkus.quartz.runtime.graal; +import java.io.ByteArrayOutputStream; import java.rmi.RemoteException; +import java.sql.ResultSet; import org.quartz.core.RemotableQuartzScheduler; +import org.quartz.impl.jdbcjobstore.StdJDBCDelegate; import com.oracle.svm.core.annotate.Substitute; import com.oracle.svm.core.annotate.TargetClass; @@ -37,5 +40,30 @@ protected RemotableQuartzScheduler getRemoteScheduler() { } +@TargetClass(StdJDBCDelegate.class) +final class Target_org_quartz_impl_jdbc_jobstore_StdJDBCDelegate { + + /** + * Activate the usage of {@link java.util.Properties} to avoid Object serialization + * which is not supported by GraalVM - see https://github.com/oracle/graal/issues/460 + * + * @return true + */ + @Substitute + protected boolean canUseProperties() { + return true; + } + + @Substitute + protected ByteArrayOutputStream serializeObject(Object obj) { + throw new IllegalStateException("Object serialization not supported."); // should not reach here + } + + @Substitute + protected Object getObjectFromBlob(ResultSet rs, String colName) { + throw new IllegalStateException("Object serialization not supported."); // should not reach here + } +} + final class QuartzSubstitutions { } diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 7fc88094f682f..4b5bdea0b3a70 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -86,6 +86,7 @@ elytron-security-jdbc vertx-graphql jpa-without-entity + quartz diff --git a/integration-tests/quartz/pom.xml b/integration-tests/quartz/pom.xml new file mode 100644 index 0000000000000..d0581b8d737f9 --- /dev/null +++ b/integration-tests/quartz/pom.xml @@ -0,0 +1,131 @@ + + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + ../ + + 4.0.0 + + quarkus-integration-test-quartz + Quarkus - Integration Tests - Quartz + The Quartz integration test module + + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-resteasy + + + io.quarkus + quarkus-quartz + + + + io.quarkus + quarkus-agroal + + + + io.quarkus + quarkus-flyway + + + + io.quarkus + quarkus-jdbc-h2 + + + + + io.quarkus + quarkus-junit5 + test + + + io.quarkus + quarkus-test-h2 + test + + + io.rest-assured + rest-assured + test + + + + + + + io.quarkus + quarkus-maven-plugin + ${project.version} + + + + build + + + + + + + + + + native-image + + + native + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + + ${project.build.directory}/${project.build.finalName}-runner + + + + + + + io.quarkus + quarkus-maven-plugin + ${project.version} + + + native-image + + native-image + + + true + true + ${graalvmHome} + + + + + + + + + + diff --git a/integration-tests/quartz/src/main/java/io/quarkus/it/quartz/CountResource.java b/integration-tests/quartz/src/main/java/io/quarkus/it/quartz/CountResource.java new file mode 100644 index 0000000000000..fc0aef6fc913a --- /dev/null +++ b/integration-tests/quartz/src/main/java/io/quarkus/it/quartz/CountResource.java @@ -0,0 +1,20 @@ +package io.quarkus.it.quartz; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +@Path("/scheduler/count") +public class CountResource { + + @Inject + Counter counter; + + @GET + @Produces(MediaType.TEXT_PLAIN) + public Integer getCount() { + return counter.get(); + } +} diff --git a/integration-tests/quartz/src/main/java/io/quarkus/it/quartz/Counter.java b/integration-tests/quartz/src/main/java/io/quarkus/it/quartz/Counter.java new file mode 100644 index 0000000000000..f5aa6745f023e --- /dev/null +++ b/integration-tests/quartz/src/main/java/io/quarkus/it/quartz/Counter.java @@ -0,0 +1,29 @@ +package io.quarkus.it.quartz; + +import java.util.concurrent.atomic.AtomicInteger; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.ApplicationScoped; + +import io.quarkus.scheduler.Scheduled; + +@ApplicationScoped +public class Counter { + + AtomicInteger counter; + + @PostConstruct + void init() { + counter = new AtomicInteger(); + } + + public int get() { + return counter.get(); + } + + @Scheduled(cron = "0/1 * * * * ?") + void increment() { + counter.incrementAndGet(); + } + +} \ No newline at end of file diff --git a/integration-tests/quartz/src/main/resources/application.properties b/integration-tests/quartz/src/main/resources/application.properties new file mode 100644 index 0000000000000..6107a8ad8a3a9 --- /dev/null +++ b/integration-tests/quartz/src/main/resources/application.properties @@ -0,0 +1,17 @@ +# datasource configuration +quarkus.datasource.url=jdbc:h2:tcp://localhost/mem:test +quarkus.datasource.driver=org.h2.Driver +quarkus.datasource.max-size=8 +quarkus.datasource.min-size=2 + +# Quartz configuration +quarkus.quartz.store-type=db +quarkus.quartz.clustered=true + +# flyway to create Quartz tables +quarkus.flyway.connect-retries=10 +quarkus.flyway.table=flyway_quarkus_history +quarkus.flyway.migrate-at-start=true +quarkus.flyway.baseline-on-migrate=true +quarkus.flyway.baseline-version=1.0 +quarkus.flyway.baseline-description=Quartz diff --git a/integration-tests/quartz/src/main/resources/db/migration/V1.0.1__QuarkusQuartz.sql b/integration-tests/quartz/src/main/resources/db/migration/V1.0.1__QuarkusQuartz.sql new file mode 100644 index 0000000000000..b362b7e53fc07 --- /dev/null +++ b/integration-tests/quartz/src/main/resources/db/migration/V1.0.1__QuarkusQuartz.sql @@ -0,0 +1,238 @@ +CREATE TABLE QRTZ_CALENDARS ( + SCHED_NAME VARCHAR(120) NOT NULL, + CALENDAR_NAME VARCHAR (200) NOT NULL , + CALENDAR IMAGE NOT NULL +); + +CREATE TABLE QRTZ_CRON_TRIGGERS ( + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_NAME VARCHAR (200) NOT NULL , + TRIGGER_GROUP VARCHAR (200) NOT NULL , + CRON_EXPRESSION VARCHAR (120) NOT NULL , + TIME_ZONE_ID VARCHAR (80) +); + +CREATE TABLE QRTZ_FIRED_TRIGGERS ( + SCHED_NAME VARCHAR(120) NOT NULL, + ENTRY_ID VARCHAR (95) NOT NULL , + TRIGGER_NAME VARCHAR (200) NOT NULL , + TRIGGER_GROUP VARCHAR (200) NOT NULL , + INSTANCE_NAME VARCHAR (200) NOT NULL , + FIRED_TIME BIGINT NOT NULL , + SCHED_TIME BIGINT NOT NULL , + PRIORITY INTEGER NOT NULL , + STATE VARCHAR (16) NOT NULL, + JOB_NAME VARCHAR (200) NULL , + JOB_GROUP VARCHAR (200) NULL , + IS_NONCONCURRENT BOOLEAN NULL , + REQUESTS_RECOVERY BOOLEAN NULL +); + +CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS ( + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_GROUP VARCHAR (200) NOT NULL +); + +CREATE TABLE QRTZ_SCHEDULER_STATE ( + SCHED_NAME VARCHAR(120) NOT NULL, + INSTANCE_NAME VARCHAR (200) NOT NULL , + LAST_CHECKIN_TIME BIGINT NOT NULL , + CHECKIN_INTERVAL BIGINT NOT NULL +); + +CREATE TABLE QRTZ_LOCKS ( + SCHED_NAME VARCHAR(120) NOT NULL, + LOCK_NAME VARCHAR (40) NOT NULL +); + +CREATE TABLE QRTZ_JOB_DETAILS ( + SCHED_NAME VARCHAR(120) NOT NULL, + JOB_NAME VARCHAR (200) NOT NULL , + JOB_GROUP VARCHAR (200) NOT NULL , + DESCRIPTION VARCHAR (250) NULL , + JOB_CLASS_NAME VARCHAR (250) NOT NULL , + IS_DURABLE BOOLEAN NOT NULL , + IS_NONCONCURRENT BOOLEAN NOT NULL , + IS_UPDATE_DATA BOOLEAN NOT NULL , + REQUESTS_RECOVERY BOOLEAN NOT NULL , + JOB_DATA IMAGE NULL +); + +CREATE TABLE QRTZ_SIMPLE_TRIGGERS ( + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_NAME VARCHAR (200) NOT NULL , + TRIGGER_GROUP VARCHAR (200) NOT NULL , + REPEAT_COUNT BIGINT NOT NULL , + REPEAT_INTERVAL BIGINT NOT NULL , + TIMES_TRIGGERED BIGINT NOT NULL +); + +CREATE TABLE QRTZ_SIMPROP_TRIGGERS ( + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_NAME VARCHAR(200) NOT NULL, + TRIGGER_GROUP VARCHAR(200) NOT NULL, + STR_PROP_1 VARCHAR(512) NULL, + STR_PROP_2 VARCHAR(512) NULL, + STR_PROP_3 VARCHAR(512) NULL, + INT_PROP_1 INTEGER NULL, + INT_PROP_2 INTEGER NULL, + LONG_PROP_1 BIGINT NULL, + LONG_PROP_2 BIGINT NULL, + DEC_PROP_1 NUMERIC(13,4) NULL, + DEC_PROP_2 NUMERIC(13,4) NULL, + BOOL_PROP_1 BOOLEAN NULL, + BOOL_PROP_2 BOOLEAN NULL +); + +CREATE TABLE QRTZ_BLOB_TRIGGERS ( + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_NAME VARCHAR (200) NOT NULL , + TRIGGER_GROUP VARCHAR (200) NOT NULL , + BLOB_DATA IMAGE NULL +); + +CREATE TABLE QRTZ_TRIGGERS ( + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_NAME VARCHAR (200) NOT NULL , + TRIGGER_GROUP VARCHAR (200) NOT NULL , + JOB_NAME VARCHAR (200) NOT NULL , + JOB_GROUP VARCHAR (200) NOT NULL , + DESCRIPTION VARCHAR (250) NULL , + NEXT_FIRE_TIME BIGINT NULL , + PREV_FIRE_TIME BIGINT NULL , + PRIORITY INTEGER NULL , + TRIGGER_STATE VARCHAR (16) NOT NULL , + TRIGGER_TYPE VARCHAR (8) NOT NULL , + START_TIME BIGINT NOT NULL , + END_TIME BIGINT NULL , + CALENDAR_NAME VARCHAR (200) NULL , + MISFIRE_INSTR SMALLINT NULL , + JOB_DATA IMAGE NULL +); + +ALTER TABLE QRTZ_CALENDARS ADD + CONSTRAINT PK_QRTZ_CALENDARS PRIMARY KEY + ( + SCHED_NAME, + CALENDAR_NAME + ); + +ALTER TABLE QRTZ_CRON_TRIGGERS ADD + CONSTRAINT PK_QRTZ_CRON_TRIGGERS PRIMARY KEY + ( + SCHED_NAME, + TRIGGER_NAME, + TRIGGER_GROUP + ); + +ALTER TABLE QRTZ_FIRED_TRIGGERS ADD + CONSTRAINT PK_QRTZ_FIRED_TRIGGERS PRIMARY KEY + ( + SCHED_NAME, + ENTRY_ID + ); + +ALTER TABLE QRTZ_PAUSED_TRIGGER_GRPS ADD + CONSTRAINT PK_QRTZ_PAUSED_TRIGGER_GRPS PRIMARY KEY + ( + SCHED_NAME, + TRIGGER_GROUP + ); + +ALTER TABLE QRTZ_SCHEDULER_STATE ADD + CONSTRAINT PK_QRTZ_SCHEDULER_STATE PRIMARY KEY + ( + SCHED_NAME, + INSTANCE_NAME + ); + +ALTER TABLE QRTZ_LOCKS ADD + CONSTRAINT PK_QRTZ_LOCKS PRIMARY KEY + ( + SCHED_NAME, + LOCK_NAME + ); + +ALTER TABLE QRTZ_JOB_DETAILS ADD + CONSTRAINT PK_QRTZ_JOB_DETAILS PRIMARY KEY + ( + SCHED_NAME, + JOB_NAME, + JOB_GROUP + ); + +ALTER TABLE QRTZ_SIMPLE_TRIGGERS ADD + CONSTRAINT PK_QRTZ_SIMPLE_TRIGGERS PRIMARY KEY + ( + SCHED_NAME, + TRIGGER_NAME, + TRIGGER_GROUP + ); + +ALTER TABLE QRTZ_SIMPROP_TRIGGERS ADD + CONSTRAINT PK_QRTZ_SIMPROP_TRIGGERS PRIMARY KEY + ( + SCHED_NAME, + TRIGGER_NAME, + TRIGGER_GROUP + ); + +ALTER TABLE QRTZ_TRIGGERS ADD + CONSTRAINT PK_QRTZ_TRIGGERS PRIMARY KEY + ( + SCHED_NAME, + TRIGGER_NAME, + TRIGGER_GROUP + ); + +ALTER TABLE QRTZ_CRON_TRIGGERS ADD + CONSTRAINT FK_QRTZ_CRON_TRIGGERS_QRTZ_TRIGGERS FOREIGN KEY + ( + SCHED_NAME, + TRIGGER_NAME, + TRIGGER_GROUP + ) REFERENCES QRTZ_TRIGGERS ( + SCHED_NAME, + TRIGGER_NAME, + TRIGGER_GROUP + ) ON DELETE CASCADE; + + +ALTER TABLE QRTZ_SIMPLE_TRIGGERS ADD + CONSTRAINT FK_QRTZ_SIMPLE_TRIGGERS_QRTZ_TRIGGERS FOREIGN KEY + ( + SCHED_NAME, + TRIGGER_NAME, + TRIGGER_GROUP + ) REFERENCES QRTZ_TRIGGERS ( + SCHED_NAME, + TRIGGER_NAME, + TRIGGER_GROUP + ) ON DELETE CASCADE; + +ALTER TABLE QRTZ_SIMPROP_TRIGGERS ADD + CONSTRAINT FK_QRTZ_SIMPROP_TRIGGERS_QRTZ_TRIGGERS FOREIGN KEY + ( + SCHED_NAME, + TRIGGER_NAME, + TRIGGER_GROUP + ) REFERENCES QRTZ_TRIGGERS ( + SCHED_NAME, + TRIGGER_NAME, + TRIGGER_GROUP + ) ON DELETE CASCADE; + + +ALTER TABLE QRTZ_TRIGGERS ADD + CONSTRAINT FK_QRTZ_TRIGGERS_QRTZ_JOB_DETAILS FOREIGN KEY + ( + SCHED_NAME, + JOB_NAME, + JOB_GROUP + ) REFERENCES QRTZ_JOB_DETAILS ( + SCHED_NAME, + JOB_NAME, + JOB_GROUP + ); + +COMMIT; diff --git a/integration-tests/quartz/src/test/java/io/quarkus/it/quartz/QuartzITCase.java b/integration-tests/quartz/src/test/java/io/quarkus/it/quartz/QuartzITCase.java new file mode 100644 index 0000000000000..075e767cfb937 --- /dev/null +++ b/integration-tests/quartz/src/test/java/io/quarkus/it/quartz/QuartzITCase.java @@ -0,0 +1,8 @@ +package io.quarkus.it.quartz; + +import io.quarkus.test.junit.NativeImageTest; + +@NativeImageTest +public class QuartzITCase extends QuartzTestCase { + +} diff --git a/integration-tests/quartz/src/test/java/io/quarkus/it/quartz/QuartzTestCase.java b/integration-tests/quartz/src/test/java/io/quarkus/it/quartz/QuartzTestCase.java new file mode 100644 index 0000000000000..758d80e6e3ab2 --- /dev/null +++ b/integration-tests/quartz/src/test/java/io/quarkus/it/quartz/QuartzTestCase.java @@ -0,0 +1,28 @@ +package io.quarkus.it.quartz; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.response.Response; + +@QuarkusTest +public class QuartzTestCase { + + @Test + public void testCount() throws InterruptedException { + // Wait at least 1 second + Thread.sleep(1000); + Response response = given() + .when().get("/scheduler/count"); + String body = response.asString(); + int count = Integer.valueOf(body); + assertTrue(count > 0); + response + .then() + .statusCode(200); + } + +} diff --git a/integration-tests/quartz/src/test/java/io/quarkus/it/quartz/TestResources.java b/integration-tests/quartz/src/test/java/io/quarkus/it/quartz/TestResources.java new file mode 100644 index 0000000000000..ecc74f854233f --- /dev/null +++ b/integration-tests/quartz/src/test/java/io/quarkus/it/quartz/TestResources.java @@ -0,0 +1,8 @@ +package io.quarkus.it.quartz; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.h2.H2DatabaseTestResource; + +@QuarkusTestResource(H2DatabaseTestResource.class) +public class TestResources { +} From d53fd9b8f7b9df82fb2f6424a1264e69889c91d5 Mon Sep 17 00:00:00 2001 From: Manyanda Chitimbo Date: Sat, 23 Nov 2019 20:53:06 +0100 Subject: [PATCH 2/2] docs: add Quartz documentation --- docs/src/main/asciidoc/index.adoc | 1 + docs/src/main/asciidoc/quartz.adoc | 340 ++++++++++++++++++ docs/src/main/asciidoc/scheduler.adoc | 2 + .../resources/META-INF/quarkus-extension.yaml | 5 +- 4 files changed, 346 insertions(+), 2 deletions(-) create mode 100644 docs/src/main/asciidoc/quartz.adoc diff --git a/docs/src/main/asciidoc/index.adoc b/docs/src/main/asciidoc/index.adoc index 0ab71bddc98f6..b8e96aa79ebaa 100644 --- a/docs/src/main/asciidoc/index.adoc +++ b/docs/src/main/asciidoc/index.adoc @@ -20,6 +20,7 @@ include::quarkus-intro.adoc[tag=intro] * link:lifecycle.html[Application Initialization and Termination] * link:rest-json.html[Writing JSON REST Services] * link:scheduler.html[Schedule Periodic Tasks] +* link:quartz.html[Schedule Periodic Tasks with Quartz] * link:websockets.html[Using Websockets] * link:validation.html[Validation with Hibernate Validator] * link:transaction.html[Using Transactions] diff --git a/docs/src/main/asciidoc/quartz.adoc b/docs/src/main/asciidoc/quartz.adoc new file mode 100644 index 0000000000000..ccf96c73e9ab0 --- /dev/null +++ b/docs/src/main/asciidoc/quartz.adoc @@ -0,0 +1,340 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc +//// += Quarkus - Scheduling Periodic Tasks with Quartz + +include::./attributes.adoc[] + +Modern applications often need to run specific tasks periodically. +In this guide, you learn how to schedule periodic clustered tasks using the http://www.quartz-scheduler.org/[Quartz] extension. + +[NOTE] +==== +This extension is considered `preview`. +API or configuration properties might change as the extension matures. +Feedback is welcome on our https://groups.google.com/d/forum/quarkus-dev[mailing list] or as issues in our https://github.com/quarkusio/quarkus/issues[GitHub issue tracker]. +==== + +TIP: If you only need to run in-memory scheduler use the link:scheduler[Scheduler] extension. + +== Prerequisites + +To complete this guide, you need: + +* less than 10 minutes +* an IDE +* JDK 1.8+ installed with `JAVA_HOME` configured appropriately +* Apache Maven 3.5.3+ +* Docker and Docker Compose installed on your machine + +== Architecture + +In this guide, we are going to expose one Rest API `tasks` to visualise the list of tasks created by a Quartz job running every 10 seconds. + +== Solution + +We recommend that you follow the instructions in the next sections and create the application step by step. +However, you can go right to the completed example. + +Clone the Git repository: `git clone {quickstarts-clone-url}`, or download an {quickstarts-archive-url}[archive]. + +The solution is located in the `quartz-quickstart` {quickstarts-tree-url}/quartz-quickstart[directory]. + +== Creating the Maven project + +First, we need a new project. Create a new project with the following command: + +[source,shell,subs=attributes+] +---- +mvn io.quarkus:quarkus-maven-plugin:{quarkus-version}:create \ + -DprojectGroupId=org.acme \ + -DprojectArtifactId=quartz-quickstart \ + -DclassName="org.acme.quartz.TaskResource" \ + -Dpath="/tasks" \ + -Dextensions="quartz, hibernate-orm-panache, flyway, resteasy-jsonb, jdbc-postgresql" +cd quartz-quickstart +---- + +It generates: + +* the Maven structure +* a landing page accessible on `http://localhost:8080` +* example `Dockerfile` files for both `native` and `jvm` modes +* the application configuration file +* an `org.acme.quartz.TaskResource` resource +* an associated test + +The Maven project also imports the Quarkus Quartz extension. + +== Creating the Task Entity + +In the `org.acme.quartz` package, create the `Task` class, with the following content: + +[source,java] +---- +package org.acme.quartz; + +import javax.persistence.Entity; +import java.time.Instant; +import javax.persistence.Table; + +import io.quarkus.hibernate.orm.panache.PanacheEntity; + +@Entity +@Table(name="TASKS") +public class Task extends PanacheEntity { <1> + public Instant createdAt; + + public Task() { + createdAt = Instant.now(); + } + + public Task(Instant time) { + this.createdAt = time; + } +} +---- +1. Declare the entity using https://quarkus.io/guides/hibernate-orm-panache[Panache] + +== Creating a scheduled job + +In the `org.acme.quartz` package, create the `TaskBean` class, with the following content: + +[source,java] +---- +package org.acme.quartz; + +import javax.enterprise.context.ApplicationScoped; + +import javax.transaction.Transactional; + +import io.quarkus.scheduler.Scheduled; + +@ApplicationScoped <1> +public class TaskBean { + + @Transactional + @Scheduled(every = "10s") <2> + void schedule() { + Task task = new Task(); <3> + task.persist(); <4> + } +} +---- +1. Declare the bean in the _application_ scope +2. Use the `@Scheduled` annotation to instruct Quarkus to run this method every 10 seconds. +3. Create a new `Task` with the current start time. +4. Persist the task in database using https://quarkus.io/guides/hibernate-orm-panache[Panache]. + +== Updating the application configuration file + +Edit the `application.properties` file and add the below configuration: +[source,shell] +---- +# Quartz configuration +quarkus.quartz.clustered=true <1> +quarkus.quartz.store-type=db <2> + +# Datasource configuration. +quarkus.datasource.url=jdbc:postgresql://localhost/quarkus_test +quarkus.datasource.driver=org.postgresql.Driver +quarkus.datasource.username=quarkus_test +quarkus.datasource.password=quarkus_test + +# Hibernate configuration +quarkus.hibernate-orm.database.generation=none +quarkus.hibernate-orm.log.sql=true +quarkus.hibernate-orm.sql-load-script=no-file + +# flyway configuration +quarkus.flyway.connect-retries=10 +quarkus.flyway.table=flyway_quarkus_history +quarkus.flyway.migrate-at-start=true +quarkus.flyway.baseline-on-migrate=true +quarkus.flyway.baseline-version=1.0 +quarkus.flyway.baseline-description=Quartz +---- + +1. Indicate that the scheduler will be run in clustered mode +2. Use the database store to persist job related information so that they can be shared between nodes + +== Updating the resource and the test + +Edit the `TaskResource` class, and update the content to: + +[source,java] +---- +package org.acme.quartz; + +import java.util.List; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +@Path("/tasks") +@Produces(MediaType.APPLICATION_JSON) +public class TaskResource { + + @GET + public List listAll() { + return Task.listAll(); <1> + } +} +---- +1. Retrieve the list of created tasks from the database + +We also need to update the tests. Edit the `TaskResourceTest` class to match: + +[source,java] +---- +package org.acme.quartz; + +import io.quarkus.test.junit.QuarkusTest; + +import static org.hamcrest.Matchers.greaterThanOrEqualTo; + +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; + +@QuarkusTest +public class TaskResourceTest { + + @Test + public void tasks() throws InterruptedException { + Thread.sleep(1000); // wait at least a second to have the first task created + given() + .when().get("/tasks") + .then() + .statusCode(200) + .body("size()", is(greaterThanOrEqualTo(1))); <1> + } +} +---- +1. Ensure that we have a `200` response and at least one task created + +== Creating Quartz Tables + +Add a SQL migration file named `src/main/resources/db/migration/V2.0.0__QuarkusQuartzTasks.sql` with the content copied from +file with the content from {quickstarts-blob-url}/quartz-quickstart/src/main/resources/db/migration/V2.0.0__QuarkusQuartzTasks.sql[V2.0.0__QuarkusQuartzTasks.sql]. + +== Configuring the load balancer + +In the root directory, create a `nginx.conf` file with the following content: + +[source,conf] +---- +user nginx; + +events { + worker_connections 1000; +} + +http { + server { + listen 8080; + location / { + proxy_pass http://tasks:8080; <1> + } + } +} +---- + +1. Route all traffic to our tasks application + +== Setting Application Deployment + +In the root directory, create a `docker-compose.yml` file with the following content: + +[source,yaml] +---- +version: '3' + +services: + tasks: <1> + image: quarkus-quickstarts/quartz:1.0 + build: + context: ./ + dockerfile: src/main/docker/Dockerfile.${QUARKUS_MODE:-jvm} + environment: + QUARKUS_DATASOURCE_URL: jdbc:postgresql://postgres/quarkus_test + networks: + - tasks-network + depends_on: + - postgres + + nginx: <2> + image: nginx:1.17.6 + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + depends_on: + - tasks + ports: + - 8080:8080 + networks: + - tasks-network + + postgres: <3> + image: postgres:11.3 + container_name: quarkus_test + environment: + - POSTGRES_USER=quarkus_test + - POSTGRES_PASSWORD=quarkus_test + - POSTGRES_DB=quarkus_test + ports: + - 5432:5432 + networks: + - tasks-network + +networks: + tasks-network: + driver: bridge +---- + +1. Define the tasks service +2. Define the nginx load balancer to route incoming traffic to an appropriate node +3. Define the configuration to run the database + +== Running the database + +In a separate terminal, run the below command: + +[source,shell] +---- +docker-compose up postgres <1> +---- + +1. Start the database instance using the configuration options supplied in the `docker-compose.yml` file + +== Run the application in Dev Mode + +Run the application with: `./mvnw quarkus:dev`. +After a few seconds, open another terminal and run `curl localhost:8080/tasks` to verify that we have at least one task created. + +As usual, the application can be packaged using `./mvnw clean package` and executed using the `-runner.jar` file. +You can also generate the native executable with `./mvnw clean package -Pnative`. + +== Packaging the application and run several instances + +The application can be packaged using `./mvnw clean package`. Once the build is successful, run the below command: + +[source,shell] +---- +docker-compose up --scale tasks=2 --scale nginx=1 <1> +---- + +1. Start two instances of the application and a load balancer + +After a few seconds, in another terminal, run `curl localhost:8080/tasks` to verify that tasks were only created at different instants and in an interval of 10 seconds. + +You can also generate the native executable with `./mvnw clean package -Pnative`. + +[[quartz-configuration-reference]] +== Quartz Configuration Reference + +include::{generated-dir}/config/quarkus-quartz.adoc[leveloffset=+1, opts=optional] diff --git a/docs/src/main/asciidoc/scheduler.adoc b/docs/src/main/asciidoc/scheduler.adoc index 6981be9a3e1a6..53d5423077ca6 100644 --- a/docs/src/main/asciidoc/scheduler.adoc +++ b/docs/src/main/asciidoc/scheduler.adoc @@ -10,6 +10,8 @@ include::./attributes.adoc[] Modern applications often need to run specific tasks periodically. In this guide, you learn how to schedule periodic tasks. +TIP: If you need a clustered scheduler use the link:quartz[Quartz extension]. + == Prerequisites To complete this guide, you need: diff --git a/extensions/quartz/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/quartz/runtime/src/main/resources/META-INF/quarkus-extension.yaml index a518bc3dfed0f..b34bb338a30c2 100644 --- a/extensions/quartz/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/quartz/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -1,11 +1,12 @@ --- -name: "Scheduler - tasks" +name: "Quartz" metadata: keywords: - "scheduler" + - "quartz" - "tasks" - "periodic-tasks" - guide: "https://quarkus.io/guides/scheduler" + guide: "https://quarkus.io/guides/quartz" categories: - "miscellaneous" status: "preview"