Skip to content

Commit

Permalink
feat(scheduler): add scheduler configuration and add support for clus…
Browse files Browse the repository at this point in the history
…tering

Fixes quarkusio#3520
  • Loading branch information
machi1990 committed Sep 6, 2019
1 parent c1e2ca8 commit ba7d766
Show file tree
Hide file tree
Showing 15 changed files with 664 additions and 20 deletions.
72 changes: 72 additions & 0 deletions docs/src/main/asciidoc/scheduled-guide.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,78 @@ public class CountResourceTest {
----
1. Ensure that the response contains `count`

== Scheduler configurations

`quarkus.scheduler.instance-name`:: The instance name of the scheduler.
+
Type: `java.lang.String` +
Defaults to: `DefaultQuartzScheduler` +


`quarkus.scheduler.state-store.misfire-threshold`:: The number of duration by which a trigger must have missed its next-fire-time, in order for it to be considered "misfired" and thus have its misfire instruction applied.
+
Type: `java.time.Duration` +
Defaults to: `1m` +


`quarkus.scheduler.state-store.cluster-checking-interval`:: The frequency at which this instance "checks-in" with the other instances of the cluster .This affects the rate of detecting failed instances.
+
Type: `java.time.Duration` +
Defaults to: `20s` +


`quarkus.scheduler.state-store.cluster-enabled`:: Enable cluster mode or not. This takes effect if the <<state-store-type, state store>> is `jdbc`.
+
Type: `java.lang.Boolean` +


`quarkus.scheduler.state-store.driver-delegate-class`:: The JDBC driver delegate class. This is not required if the <<state-store-type, state store>> is `in-memory`.
Otherwise it must be set to a delegate class corresponding to the SQL driver in use.
Possible values can be obtained from https://github.com/quartz-scheduler/quartz/tree/master/quartz-core/src/main/java/org/quartz/impl/jdbcjobstore[quartz-scheduler JDBC stores delegate]
+
Type: `java.lang.String` +
Defaults to: `org.quartz.impl.jdbcjobstore.StdJDBCDelegate` +

[[state-store-type]]
`quarkus.scheduler.state-store`:: The type of state store to use. Possible values are: `in-memory`, and `jdbc`.
- If set to `in-memory`, the scheduler will use the `org.quartz.simpl.RAMJobStore` job store class
- If set to `jdbc`, the scheduler will use the `org.quartz.impl.jdbcjobstore.JobStoreTX` job store class.
When using this option make sure that you have the link:datasource-guide.html[agroal datasource configured] and that <<creating-scheduling-job, scheduling tables>> exists.
This store type is not supported in Native Image because of https://github.com/oracle/graal/issues/460[unsupported Object serialization].
+
Type: `io.quarkus.scheduler.runtime.SchedulerBuildTimeConfig.StoreType` +
Defaults to: `in-memory` +


`quarkus.scheduler.instance-id`:: The instance id of the scheduler. This is highly required when running clustered schedulers as each node in the cluster MUST have a unique instanceId.
Defaults to `AUTO` to automatically generate unique ids for each node in the cluster
+
Type: `java.lang.String` +
Defaults to: `AUTO` +


`quarkus.scheduler.thread-count`:: The size of scheduler thread pool. This will initialise the number of worker threads in the pool
+
Type: `int` +
Defaults to: `25` +


`quarkus.scheduler.thread-priority`:: Thread priority of worker threads in the pool.
+
Type: `int` +
Defaults to: `5` +

include::duration-format-note.adoc[]

[TIP]
[[creating-scheduling-job]]
.About creating database tables when using `jdbc` state store
=====
The Quarkus scheduler does not create the necessary scheduling tables in database automatically. If these tables are missing, the schuduler will throw an excpetion during application startup.
Thus you'll need to create them. To do so, choose a file corresponding to your SQL driver from https://github.com/quartz-scheduler/quartz/tree/master/quartz-core/src/main/resources/org/quartz/impl/jdbcjobstore[quartz sql script files].
Once you are done, you can use the link:flyway-guide.html[flyway extension] to execute table creation using the choosen SQL file.
=====

== Package and run the application

Run the application with: `./mvnw compile quarkus:dev`.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.quarkus.scheduler.deployment;

import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT;
import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT;

import java.text.ParseException;
Expand All @@ -21,8 +22,12 @@
import org.jboss.jandex.Type.Kind;
import org.jboss.logging.Logger;
import org.quartz.CronExpression;
import org.quartz.impl.jdbcjobstore.JobStoreSupport;
import org.quartz.impl.jdbcjobstore.JobStoreTX;
import org.quartz.impl.jdbcjobstore.StdJDBCDelegate;
import org.quartz.simpl.CascadingClassLoadHelper;
import org.quartz.simpl.RAMJobStore;
import org.quartz.simpl.SimpleInstanceIdGenerator;
import org.quartz.simpl.SimpleThreadPool;

import io.quarkus.arc.Arc;
Expand Down Expand Up @@ -58,10 +63,14 @@
import io.quarkus.gizmo.ResultHandle;
import io.quarkus.scheduler.Scheduled;
import io.quarkus.scheduler.ScheduledExecution;
import io.quarkus.scheduler.runtime.AgroalQuartzConnectionPoolingProvider;
import io.quarkus.scheduler.runtime.QuartzScheduler;
import io.quarkus.scheduler.runtime.ScheduledInvoker;
import io.quarkus.scheduler.runtime.SchedulerBuildTimeConfig;
import io.quarkus.scheduler.runtime.SchedulerConfiguration;
import io.quarkus.scheduler.runtime.SchedulerDeploymentRecorder;
import io.quarkus.scheduler.runtime.SchedulerRuntimeConfig;
import io.quarkus.scheduler.runtime.StateStoreType;

/**
* @author Martin Kouba
Expand Down Expand Up @@ -118,9 +127,12 @@ public void transform(TransformationContext context) {
@BuildStep
List<ReflectiveClassBuildItem> reflectiveClasses() {
List<ReflectiveClassBuildItem> reflectiveClasses = new ArrayList<>();
reflectiveClasses.add(new ReflectiveClassBuildItem(false, false, CascadingClassLoadHelper.class.getName()));
reflectiveClasses.add(new ReflectiveClassBuildItem(true, false, SimpleThreadPool.class.getName()));
reflectiveClasses.add(new ReflectiveClassBuildItem(false, false, CascadingClassLoadHelper.class.getName()));
reflectiveClasses.add(new ReflectiveClassBuildItem(true, false, SimpleInstanceIdGenerator.class.getName()));
reflectiveClasses.add(new ReflectiveClassBuildItem(true, false, AgroalQuartzConnectionPoolingProvider.class.getName()));
reflectiveClasses.add(new ReflectiveClassBuildItem(true, false, RAMJobStore.class.getName()));
reflectiveClasses.add(new ReflectiveClassBuildItem(true, true, JobStoreTX.class.getName()));
return reflectiveClasses;
}

Expand Down Expand Up @@ -225,12 +237,39 @@ public void write(String name, byte[] data) {
}

@BuildStep
public void logCleanup(BuildProducer<LogCleanupFilterBuildItem> logCleanupFilter) {
@Record(RUNTIME_INIT)
public void registerConfiguration(SchedulerDeploymentRecorder recorder,
SchedulerRuntimeConfig schedulerRuntimeConfig) {
recorder.registerConfiguration(schedulerRuntimeConfig);
}

@BuildStep
@Record(STATIC_INIT)
public void registerConfiguration(SchedulerDeploymentRecorder recorder,
SchedulerBuildTimeConfig schedulerBuildTimeConfig,
BuildProducer<ReflectiveClassBuildItem> reflectiveClassBuildItemBuildProducer) {
StateStoreType stateStore = schedulerBuildTimeConfig.stateStore.type;

if (StateStoreType.JDBC.equals(stateStore)) {
reflectiveClassBuildItemBuildProducer
.produce((new ReflectiveClassBuildItem(true, false, StdJDBCDelegate.class.getName())));
reflectiveClassBuildItemBuildProducer
.produce((new ReflectiveClassBuildItem(true, false, JobStoreSupport.class.getName())));
reflectiveClassBuildItemBuildProducer.produce(new ReflectiveClassBuildItem(true, true,
schedulerBuildTimeConfig.stateStore.datasource.driverDelegateClass));
}

recorder.registerConfiguration(schedulerBuildTimeConfig);
}

@BuildStep
public void logCleanup(BuildProducer<LogCleanupFilterBuildItem> logCleanupFilter,
SchedulerBuildTimeConfig schedulerBuildTimeConfig) {
logCleanupFilter.produce(new LogCleanupFilterBuildItem("org.quartz.impl.StdSchedulerFactory",
"Quartz scheduler version:",
// no need to log if it's the default
"Using default implementation for",
"Quartz scheduler 'DefaultQuartzScheduler'"));
"Quartz scheduler '" + schedulerBuildTimeConfig.instanceName + "'"));

logCleanupFilter.produce(new LogCleanupFilterBuildItem("org.quartz.core.QuartzScheduler",
"Quartz Scheduler v",
Expand All @@ -239,9 +278,10 @@ public void logCleanup(BuildProducer<LogCleanupFilterBuildItem> logCleanupFilter
// no need to log if it's the default
"Scheduler DefaultQuartzScheduler"));

logCleanupFilter.produce(new LogCleanupFilterBuildItem("org.quartz.simpl.RAMJobStore",
"RAMJobStore initialized."));

logCleanupFilter.produce(new LogCleanupFilterBuildItem(StateStoreType.IN_MEMORY.clazz,
StateStoreType.IN_MEMORY.name + " initialized.", "Handling"));
logCleanupFilter.produce(new LogCleanupFilterBuildItem(StateStoreType.JDBC.clazz,
StateStoreType.JDBC.name + " initialized.", "Handling"));
logCleanupFilter.produce(new LogCleanupFilterBuildItem("org.quartz.core.SchedulerSignalerImpl",
"Initialized Scheduler Signaller of type"));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.quarkus.scheduler.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.test.QuarkusUnitTest;

public class NoDefaultDataSourceErrorTest {

@RegisterExtension
static final QuarkusUnitTest testNoDefaultDatasourceError = new QuarkusUnitTest()
.setExpectedException(IllegalStateException.class)
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addClasses(SimpleJobs.class)
.addAsResource(new StringAsset("simpleJobs.cron=0/1 * * * * ?\nsimpleJobs.every=1s" +
"\nquarkus.scheduler.state-store=jdbc\n"),
"application.properties"));

@Test
public void shouldFailMissingDataSource() throws InterruptedException {
/**
* Should not reach here
*/
Assertions.fail();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.quarkus.scheduler.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.test.QuarkusUnitTest;

public class NoNamedDataSourceErrorTest {

@RegisterExtension
static final QuarkusUnitTest testNoNamedDatasourceError = new QuarkusUnitTest()
.setExpectedException(IllegalStateException.class)
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addClasses(SimpleJobs.class)
.addAsResource(new StringAsset("simpleJobs.cron=0/1 * * * * ?" +
"\nsimpleJobs.every=1s" +
"\nquarkus.scheduler.state-store=jdbc\n" +
"\nquarkus.scheduler.state-store.datasource.name=ds-name\n"),
"application.properties"));

@Test
public void shouldFailMissingDataSource() throws InterruptedException {
/**
* Should not reach here
*/
Assertions.fail();
}

}
4 changes: 4 additions & 0 deletions extensions/scheduler/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-core</artifactId>
</dependency>
<dependency>
<groupId>com.oracle.substratevm</groupId>
<artifactId>svm</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package io.quarkus.scheduler.runtime;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;

import javax.sql.DataSource;

import org.quartz.utils.PoolingConnectionProvider;

import io.quarkus.arc.Arc;
import io.quarkus.arc.InstanceHandle;

public class AgroalQuartzConnectionPoolingProvider implements PoolingConnectionProvider {
final private DataSource dataSource;

public AgroalQuartzConnectionPoolingProvider() {
SchedulerBuildTimeConfig.SchedulerDatasourceConfig dataSourceConfig = SchedulerConfigHolder
.getSchedulerBuildTimeConfig().stateStore.datasource;
final InstanceHandle<DataSource> dataSourceInstanceHandle;

if (dataSourceConfig.name.isPresent()) {
dataSourceInstanceHandle = Arc.container().instance(dataSourceConfig.name.get());
} else {
dataSourceInstanceHandle = Arc.container().instance(DataSource.class);
}

if (dataSourceInstanceHandle.isAvailable()) {
this.dataSource = dataSourceInstanceHandle.get();
} else {
final String dataSourceName = dataSourceConfig.name.orElse("_default_");
throw new IllegalStateException(
"JDBC Store configured but the datasource \"" + dataSourceName + "\" is missing. "
+ "You can configure your "
+ "datasource by following the guide available at: https://quarkus.io/guides/datasource-guide");
}
}

@SuppressWarnings("unused")
public AgroalQuartzConnectionPoolingProvider(Properties properties) {
this();
}

@Override
public DataSource getDataSource() {
return dataSource;
}

@Override
public Connection getConnection() throws SQLException {
return dataSource.getConnection();
}

@Override
public void shutdown() {
}

@Override
public void initialize() {

}
}
Loading

0 comments on commit ba7d766

Please sign in to comment.