Skip to content

Commit 42242d4

Browse files
authored
Merge pull request #48579 from manovotn/issue48545
Quartz - Defer driver discovery to runtime in order to provide more flexibility to users
2 parents b6a5d7d + c87230c commit 42242d4

27 files changed

+1232
-36
lines changed
Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,40 @@
11
package io.quarkus.quartz.deployment;
22

3+
import java.util.List;
34
import java.util.Optional;
45

56
import io.quarkus.builder.item.SimpleBuildItem;
7+
import io.quarkus.quartz.runtime.jdbc.JDBCDataSource;
68

79
/**
8-
* Holds the SQL driver dialect {@link org.quartz.impl.jdbcjobstore.StdJDBCDelegate driver delegate} to use.
10+
* Holds the necessary information to determine SQL driver dialect.
11+
* <p>
12+
* This can mean either a custom driver registered by user or one of Quarkus built-in drivers.
13+
* If it is the latter, we defer discovering the driver to runtime, see also {@link io.quarkus.quartz.runtime.QuartzSupport}.
914
*/
1015
final class QuartzJDBCDriverDialectBuildItem extends SimpleBuildItem {
1116
private final Optional<String> driver;
17+
private final List<JDBCDataSource> dataSources;
1218

13-
public QuartzJDBCDriverDialectBuildItem(Optional<String> driver) {
19+
/**
20+
* Driver represented as a {@code String}, can be empty even if configured during build time in case there is no DB.
21+
* <p>
22+
* The list of data sources are only used if the driver needs to be determined during runtime.
23+
* The list can be null if the driver has been determined during build time.
24+
*
25+
* @param driver driver to be used, optionally empty
26+
* @param dataSources can be null which indicates that the driver has been resolved during build time
27+
*/
28+
public QuartzJDBCDriverDialectBuildItem(Optional<String> driver, List<JDBCDataSource> dataSources) {
1429
this.driver = driver;
30+
this.dataSources = dataSources;
1531
}
1632

1733
public Optional<String> getDriver() {
1834
return driver;
1935
}
36+
37+
public List<JDBCDataSource> getDataSources() {
38+
return dataSources;
39+
}
2040
}

extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/QuartzProcessor.java

Lines changed: 56 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
import io.quarkus.quartz.runtime.QuartzRecorder;
6565
import io.quarkus.quartz.runtime.QuartzSchedulerImpl;
6666
import io.quarkus.quartz.runtime.QuartzSupport;
67+
import io.quarkus.quartz.runtime.jdbc.JDBCDataSource;
6768
import io.quarkus.quartz.runtime.jdbc.QuarkusDBv8Delegate;
6869
import io.quarkus.quartz.runtime.jdbc.QuarkusHSQLDBDelegate;
6970
import io.quarkus.quartz.runtime.jdbc.QuarkusMSSQLDelegate;
@@ -121,8 +122,8 @@ QuartzJDBCDriverDialectBuildItem driver(List<JdbcDataSourceBuildItem> jdbcDataSo
121122
if (config.clustered()) {
122123
throw new ConfigurationException("Clustered jobs configured with unsupported job store option");
123124
}
124-
125-
return new QuartzJDBCDriverDialectBuildItem(Optional.empty());
125+
// No DB storage, the driver can stay empty, and we don't need data sources either
126+
return new QuartzJDBCDriverDialectBuildItem(Optional.empty(), null);
126127
}
127128

128129
if (capabilities.isMissing(Capability.AGROAL)) {
@@ -161,29 +162,45 @@ QuartzJDBCDriverDialectBuildItem driver(List<JdbcDataSourceBuildItem> jdbcDataSo
161162
throw new ConfigurationException(message);
162163
}
163164
}
165+
// A custom delegate implementation, we don't need to check datasources
166+
return new QuartzJDBCDriverDialectBuildItem(driverDelegate, null);
164167
} else {
165-
Optional<JdbcDataSourceBuildItem> selectedJdbcDataSourceBuildItem = jdbcDataSourceBuildItems.stream()
166-
.filter(i -> config.dataSourceName().isPresent() ? config.dataSourceName().get().equals(i.getName())
167-
: i.isDefault())
168-
.findFirst();
168+
if (config.deferDatasourceCheck()) {
169+
// if defer is set to true and there is a DS name, throw an exception
170+
if (config.dataSourceName().isPresent()) {
171+
String message = String.format(
172+
"Quartz datasource resolution can be either deferred to runtime or specified at build time but not both. Related properties are quarkus.quartz.defer-datasource-check=%s and quarkus.quartz.datasource=%s",
173+
config.deferDatasourceCheck(), config.dataSourceName());
174+
throw new ConfigurationException(message);
175+
}
176+
// Defer driver resolution to runtime
177+
List<JDBCDataSource> dataSources = new ArrayList<>();
178+
for (JdbcDataSourceBuildItem jdbcDataSourceBuildItem : jdbcDataSourceBuildItems) {
179+
dataSources.add(new JDBCDataSource(jdbcDataSourceBuildItem.getName(), jdbcDataSourceBuildItem.isDefault(),
180+
jdbcDataSourceBuildItem.getDbKind()));
181+
}
182+
return new QuartzJDBCDriverDialectBuildItem(Optional.empty(), dataSources);
183+
} else {
184+
// Perform driver resolution at build time
185+
Optional<JdbcDataSourceBuildItem> selectedJdbcDataSourceBuildItem = jdbcDataSourceBuildItems.stream()
186+
.filter(i -> config.dataSourceName().isPresent() ? config.dataSourceName().get().equals(i.getName())
187+
: i.isDefault())
188+
.findFirst();
169189

170-
if (!selectedJdbcDataSourceBuildItem.isPresent()) {
171-
String message = String.format(
172-
"JDBC Store configured but the '%s' datasource is not configured properly. You can configure your datasource by following the guide available at: https://quarkus.io/guides/datasource",
173-
config.dataSourceName().isPresent() ? config.dataSourceName().get() : "default");
174-
throw new ConfigurationException(message);
190+
if (!selectedJdbcDataSourceBuildItem.isPresent()) {
191+
String message = String.format(
192+
"JDBC Store configured but the '%s' datasource is not configured properly. You can configure your datasource by following the guide available at: https://quarkus.io/guides/datasource",
193+
config.dataSourceName().isPresent() ? config.dataSourceName().get() : "default");
194+
throw new ConfigurationException(message);
195+
}
196+
return new QuartzJDBCDriverDialectBuildItem(Optional.of(guessDriver(selectedJdbcDataSourceBuildItem.get())),
197+
null);
175198
}
176-
driverDelegate = Optional.of(guessDriver(selectedJdbcDataSourceBuildItem));
177199
}
178-
return new QuartzJDBCDriverDialectBuildItem(driverDelegate);
179200
}
180201

181-
private String guessDriver(Optional<JdbcDataSourceBuildItem> jdbcDataSource) {
182-
if (!jdbcDataSource.isPresent()) {
183-
return QuarkusStdJDBCDelegate.class.getName();
184-
}
185-
186-
String dataSourceKind = jdbcDataSource.get().getDbKind();
202+
private String guessDriver(JdbcDataSourceBuildItem jdbcDataSource) {
203+
String dataSourceKind = jdbcDataSource.getDbKind();
187204
if (DatabaseKind.isPostgreSQL(dataSourceKind)) {
188205
return QuarkusPostgreSQLDelegate.class.getName();
189206
}
@@ -202,7 +219,7 @@ private String guessDriver(Optional<JdbcDataSourceBuildItem> jdbcDataSource) {
202219

203220
@BuildStep
204221
List<ReflectiveClassBuildItem> reflectiveClasses(QuartzBuildTimeConfig config,
205-
QuartzJDBCDriverDialectBuildItem driverDialect) {
222+
QuartzJDBCDriverDialectBuildItem driverDialect, List<JdbcDataSourceBuildItem> jdbcDataSourceBuildItems) {
206223
List<ReflectiveClassBuildItem> reflectiveClasses = new ArrayList<>();
207224

208225
if (config.serializeJobData()) {
@@ -249,12 +266,23 @@ List<ReflectiveClassBuildItem> reflectiveClasses(QuartzBuildTimeConfig config,
249266
reflectiveClasses.add(ReflectiveClassBuildItem.builder(Connection.class)
250267
.reason(getClass().getName()).methods()
251268
.fields().build());
252-
reflectiveClasses.add(ReflectiveClassBuildItem.builder(driverDialect.getDriver().get())
253-
.reason(getClass().getName())
254-
.methods().build());
255-
reflectiveClasses.add(ReflectiveClassBuildItem.builder("io.quarkus.quartz.runtime.QuartzSchedulerImpl$InvokerJob")
256-
.reason(getClass().getName())
257-
.methods().fields().build());
269+
if (driverDialect.getDriver().isPresent()) {
270+
// build time datasource resolution
271+
reflectiveClasses.add(ReflectiveClassBuildItem.builder(driverDialect.getDriver().get())
272+
.reason(getClass().getName())
273+
.methods().build());
274+
} else {
275+
// deferred datasource resolution, register all DB kinds we can derive from configuration
276+
for (JdbcDataSourceBuildItem jdbcDataSourceBuildItem : jdbcDataSourceBuildItems) {
277+
reflectiveClasses.add(ReflectiveClassBuildItem.builder(guessDriver(jdbcDataSourceBuildItem))
278+
.reason(getClass().getName())
279+
.methods().build());
280+
}
281+
reflectiveClasses
282+
.add(ReflectiveClassBuildItem.builder("io.quarkus.quartz.runtime.QuartzSchedulerImpl$InvokerJob")
283+
.reason(getClass().getName())
284+
.methods().fields().build());
285+
}
258286
}
259287

260288
reflectiveClasses
@@ -346,7 +374,8 @@ public void quartzSupportBean(
346374
syntheticBeanBuildItemBuildProducer.produce(SyntheticBeanBuildItem.configure(QuartzSupport.class)
347375
.scope(Singleton.class) // this should be @ApplicationScoped but it fails for some reason
348376
.setRuntimeInit()
349-
.supplier(recorder.quartzSupportSupplier(driverDialect.getDriver(), nonconcurrentMethods))
377+
.supplier(recorder.quartzSupportSupplier(driverDialect.getDriver(), driverDialect.getDataSources(),
378+
nonconcurrentMethods))
350379
.done());
351380
}
352381
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package io.quarkus.quartz.test;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
5+
import java.util.function.Consumer;
6+
7+
import org.jboss.shrinkwrap.api.asset.StringAsset;
8+
import org.junit.jupiter.api.Assertions;
9+
import org.junit.jupiter.api.Test;
10+
import org.junit.jupiter.api.extension.RegisterExtension;
11+
12+
import io.quarkus.builder.BuildChainBuilder;
13+
import io.quarkus.builder.BuildContext;
14+
import io.quarkus.builder.BuildStep;
15+
import io.quarkus.deployment.Capability;
16+
import io.quarkus.deployment.builditem.CapabilityBuildItem;
17+
import io.quarkus.runtime.configuration.ConfigurationException;
18+
import io.quarkus.test.QuarkusUnitTest;
19+
20+
public class InvalidDeferredDatasourceConfigurationTest {
21+
22+
@RegisterExtension
23+
static final QuarkusUnitTest test = new QuarkusUnitTest()
24+
// add a mock pretending to provide Agroal Capability to pass our validation
25+
.addBuildChainCustomizer(new Consumer<BuildChainBuilder>() {
26+
@Override
27+
public void accept(BuildChainBuilder buildChainBuilder) {
28+
buildChainBuilder.addBuildStep(new BuildStep() {
29+
@Override
30+
public void execute(BuildContext context) {
31+
context.produce(
32+
new CapabilityBuildItem(Capability.AGROAL, "fakeProvider"));
33+
}
34+
}).produces(CapabilityBuildItem.class).build();
35+
}
36+
})
37+
.assertException(t -> {
38+
assertEquals(ConfigurationException.class, t.getClass());
39+
Assertions.assertTrue(t.getMessage().contains(
40+
"Quartz datasource resolution can be either deferred to runtime or specified at build time but not both."));
41+
})
42+
.withApplicationRoot((jar) -> jar
43+
.addClasses(SimpleJobs.class)
44+
.addAsResource(new StringAsset(
45+
"quarkus.quartz.defer-datasource-check=true\n"
46+
+ "quarkus.quartz.datasource=mssql\n"
47+
+ "quarkus.quartz.store-type=jdbc-cmt"),
48+
"application.properties"));
49+
50+
@Test
51+
public void shouldFailAndNotReachHere() {
52+
Assertions.fail();
53+
}
54+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package io.quarkus.quartz.test;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
5+
import java.util.function.Consumer;
6+
7+
import jakarta.enterprise.inject.CreationException;
8+
9+
import org.jboss.shrinkwrap.api.asset.StringAsset;
10+
import org.junit.jupiter.api.Assertions;
11+
import org.junit.jupiter.api.Test;
12+
import org.junit.jupiter.api.extension.RegisterExtension;
13+
14+
import io.quarkus.builder.BuildChainBuilder;
15+
import io.quarkus.builder.BuildContext;
16+
import io.quarkus.builder.BuildStep;
17+
import io.quarkus.deployment.Capability;
18+
import io.quarkus.deployment.builditem.CapabilityBuildItem;
19+
import io.quarkus.test.QuarkusUnitTest;
20+
21+
public class InvalidDeferredDatasourceRuntimeConfigurationTest {
22+
23+
@RegisterExtension
24+
static final QuarkusUnitTest test = new QuarkusUnitTest()
25+
// add a mock pretending to provide Agroal Capability to pass our validation
26+
.addBuildChainCustomizer(new Consumer<BuildChainBuilder>() {
27+
@Override
28+
public void accept(BuildChainBuilder buildChainBuilder) {
29+
buildChainBuilder.addBuildStep(new BuildStep() {
30+
@Override
31+
public void execute(BuildContext context) {
32+
context.produce(
33+
new CapabilityBuildItem(Capability.AGROAL, "fakeProvider"));
34+
}
35+
}).produces(CapabilityBuildItem.class).build();
36+
}
37+
})
38+
.assertException(t -> {
39+
assertEquals(CreationException.class, t.getClass());
40+
Assertions.assertTrue(t.getMessage().contains(
41+
"Deferred datasource name is missing - you can configure it via quarkus.quartz.deferred-datasource-name"));
42+
})
43+
.withApplicationRoot((jar) -> jar
44+
.addClasses(SimpleJobs.class)
45+
.addAsResource(new StringAsset(
46+
"quarkus.quartz.defer-datasource-check=true\n"
47+
+ "quarkus.quartz.store-type=jdbc-cmt"),
48+
"application.properties"));
49+
50+
@Test
51+
public void shouldFailAndNotReachHere() {
52+
Assertions.fail();
53+
}
54+
}

extensions/quartz/runtime/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
<artifactId>quarkus-agroal</artifactId>
1919
<optional>true</optional>
2020
</dependency>
21+
<dependency>
22+
<groupId>io.quarkus</groupId>
23+
<artifactId>quarkus-datasource-common</artifactId>
24+
</dependency>
2125
<dependency>
2226
<groupId>io.quarkus</groupId>
2327
<artifactId>quarkus-scheduler</artifactId>

extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzBuildTimeConfig.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,18 @@ public interface QuartzBuildTimeConfig {
6565
@WithName("datasource")
6666
Optional<String> dataSourceName();
6767

68+
/**
69+
* If set to true, defers datasource check to runtime.
70+
* False by default.
71+
* <p>
72+
* Used in combination with runtime configuration {@link QuartzRuntimeConfig#deferredDatasourceName()}.
73+
* <p>
74+
* It is considered a configuration error to specify a datasource via {@link QuartzBuildTimeConfig#dataSourceName()} along
75+
* with setting this property to {@code true}.
76+
*/
77+
@WithDefault("false")
78+
boolean deferDatasourceCheck();
79+
6880
/**
6981
* The prefix for quartz job store tables.
7082
* <p>

extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzRecorder.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package io.quarkus.quartz.runtime;
22

3+
import java.util.List;
34
import java.util.Optional;
45
import java.util.Set;
56
import java.util.function.Supplier;
67

8+
import io.quarkus.quartz.runtime.jdbc.JDBCDataSource;
79
import io.quarkus.runtime.RuntimeValue;
810
import io.quarkus.runtime.annotations.Recorder;
911

@@ -19,11 +21,13 @@ public QuartzRecorder(
1921
this.runtimeConfig = runtimeConfig;
2022
}
2123

22-
public Supplier<QuartzSupport> quartzSupportSupplier(Optional<String> driverDialect, Set<String> nonconcurrentMethods) {
24+
public Supplier<QuartzSupport> quartzSupportSupplier(Optional<String> driverDialect, List<JDBCDataSource> dataSources,
25+
Set<String> nonconcurrentMethods) {
2326
return new Supplier<QuartzSupport>() {
2427
@Override
2528
public QuartzSupport get() {
26-
return new QuartzSupport(runtimeConfig.getValue(), buildTimeConfig, driverDialect, nonconcurrentMethods);
29+
return new QuartzSupport(runtimeConfig.getValue(), buildTimeConfig, driverDialect, dataSources,
30+
nonconcurrentMethods);
2731
}
2832
};
2933
}

extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzRuntimeConfig.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.time.Duration;
44
import java.util.Map;
5+
import java.util.Optional;
56

67
import io.quarkus.runtime.annotations.ConfigDocMapKey;
78
import io.quarkus.runtime.annotations.ConfigDocSection;
@@ -120,6 +121,13 @@ public interface QuartzRuntimeConfig {
120121
@WithDefault("false")
121122
boolean runBlockingScheduledMethodOnQuartzThread();
122123

124+
/**
125+
* The name of the datasource to use.
126+
* <p>
127+
* This property in valid only in combination with {@link QuartzBuildTimeConfig#deferDatasourceCheck()}
128+
*/
129+
Optional<String> deferredDatasourceName();
130+
123131
interface QuartzMisfirePolicyConfig {
124132
/**
125133
* The quartz misfire policy for this job.

extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSchedulerImpl.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,13 @@ private Properties getSchedulerConfigurationProperties(QuartzSupport quartzSuppo
592592
"" + runtimeConfig.misfireThreshold().toMillis());
593593

594594
if (buildTimeConfig.storeType().isDbStore()) {
595-
String dataSource = buildTimeConfig.dataSourceName().orElse("QUARKUS_QUARTZ_DEFAULT_DATASOURCE");
595+
String dataSource;
596+
if (buildTimeConfig.dataSourceName().isEmpty() && buildTimeConfig.deferDatasourceCheck()) {
597+
// deferred datasource resolution; pick the runtime config value
598+
dataSource = runtimeConfig.deferredDatasourceName().orElse("QUARKUS_QUARTZ_DEFAULT_DATASOURCE");
599+
} else {
600+
dataSource = buildTimeConfig.dataSourceName().orElse("QUARKUS_QUARTZ_DEFAULT_DATASOURCE");
601+
}
596602
QuarkusQuartzConnectionPoolProvider.setDataSourceName(dataSource);
597603
boolean serializeJobData = buildTimeConfig.serializeJobData();
598604
props.put(StdSchedulerFactory.PROP_JOB_STORE_USE_PROP, serializeJobData ? "false" : "true");

0 commit comments

Comments
 (0)