Re-initialize Quartz ConnectionProvider on context restart

Closes gh-35208
This commit is contained in:
Juergen Hoeller 2025-07-16 15:21:49 +02:00
parent 6f5a7eed76
commit 17382fe079
4 changed files with 73 additions and 43 deletions

View File

@ -35,6 +35,7 @@ import org.quartz.utils.DBConnectionManager;
import org.springframework.jdbc.datasource.DataSourceUtils; import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.jdbc.support.JdbcUtils; import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.jdbc.support.MetaDataAccessException; import org.springframework.jdbc.support.MetaDataAccessException;
import org.springframework.util.Assert;
/** /**
* Subclass of Quartz's {@link JobStoreCMT} class that delegates to a Spring-managed * Subclass of Quartz's {@link JobStoreCMT} class that delegates to a Spring-managed
@ -88,6 +89,8 @@ public class LocalDataSourceJobStore extends JobStoreCMT {
private @Nullable DataSource dataSource; private @Nullable DataSource dataSource;
private @Nullable DataSource nonTransactionalDataSource;
@Override @Override
@SuppressWarnings("NullAway") // Dataflow analysis limitation @SuppressWarnings("NullAway") // Dataflow analysis limitation
@ -98,11 +101,40 @@ public class LocalDataSourceJobStore extends JobStoreCMT {
throw new SchedulerConfigException("No local DataSource found for configuration - " + throw new SchedulerConfigException("No local DataSource found for configuration - " +
"'dataSource' property must be set on SchedulerFactoryBean"); "'dataSource' property must be set on SchedulerFactoryBean");
} }
// Non-transactional DataSource is optional: fall back to default
// DataSource if not explicitly specified.
this.nonTransactionalDataSource = SchedulerFactoryBean.getConfigTimeNonTransactionalDataSource();
// Configure transactional connection settings for Quartz. // Configure connection settings for Quartz.
setDataSource(TX_DATA_SOURCE_PREFIX + getInstanceName()); setDataSource(TX_DATA_SOURCE_PREFIX + getInstanceName());
setNonManagedTXDataSource(NON_TX_DATA_SOURCE_PREFIX + getInstanceName());
setDontSetAutoCommitFalse(true); setDontSetAutoCommitFalse(true);
initializeConnectionProvider();
// No, if HSQL is the platform, we really don't want to use locks...
try {
String productName = JdbcUtils.extractDatabaseMetaData(this.dataSource,
DatabaseMetaData::getDatabaseProductName);
productName = JdbcUtils.commonDatabaseName(productName);
if (productName != null && productName.toLowerCase(Locale.ROOT).contains("hsql")) {
setUseDBLocks(false);
setLockHandler(new SimpleSemaphore());
}
}
catch (MetaDataAccessException ex) {
logWarnIfNonZero(1, "Could not detect database type. Assuming locks can be taken.");
}
super.initialize(loadHelper, signaler);
}
void initializeConnectionProvider() {
final DataSource dataSourceToUse = this.dataSource;
Assert.state(dataSourceToUse != null, "DataSource must not be null");
final DataSource nonTxDataSourceToUse =
(this.nonTransactionalDataSource != null ? this.nonTransactionalDataSource : dataSourceToUse);
// Register transactional ConnectionProvider for Quartz. // Register transactional ConnectionProvider for Quartz.
DBConnectionManager.getInstance().addConnectionProvider( DBConnectionManager.getInstance().addConnectionProvider(
TX_DATA_SOURCE_PREFIX + getInstanceName(), TX_DATA_SOURCE_PREFIX + getInstanceName(),
@ -110,7 +142,7 @@ public class LocalDataSourceJobStore extends JobStoreCMT {
@Override @Override
public Connection getConnection() throws SQLException { public Connection getConnection() throws SQLException {
// Return a transactional Connection, if any. // Return a transactional Connection, if any.
return DataSourceUtils.doGetConnection(dataSource); return DataSourceUtils.doGetConnection(dataSourceToUse);
} }
@Override @Override
public void shutdown() { public void shutdown() {
@ -123,14 +155,6 @@ public class LocalDataSourceJobStore extends JobStoreCMT {
} }
); );
// Non-transactional DataSource is optional: fall back to default
// DataSource if not explicitly specified.
DataSource nonTxDataSource = SchedulerFactoryBean.getConfigTimeNonTransactionalDataSource();
final DataSource nonTxDataSourceToUse = (nonTxDataSource != null ? nonTxDataSource : this.dataSource);
// Configure non-transactional connection settings for Quartz.
setNonManagedTXDataSource(NON_TX_DATA_SOURCE_PREFIX + getInstanceName());
// Register non-transactional ConnectionProvider for Quartz. // Register non-transactional ConnectionProvider for Quartz.
DBConnectionManager.getInstance().addConnectionProvider( DBConnectionManager.getInstance().addConnectionProvider(
NON_TX_DATA_SOURCE_PREFIX + getInstanceName(), NON_TX_DATA_SOURCE_PREFIX + getInstanceName(),
@ -150,23 +174,6 @@ public class LocalDataSourceJobStore extends JobStoreCMT {
} }
} }
); );
// No, if HSQL is the platform, we really don't want to use locks...
try {
String productName = JdbcUtils.extractDatabaseMetaData(this.dataSource,
DatabaseMetaData::getDatabaseProductName);
productName = JdbcUtils.commonDatabaseName(productName);
if (productName != null && productName.toLowerCase(Locale.ROOT).contains("hsql")) {
setUseDBLocks(false);
setLockHandler(new SimpleSemaphore());
}
}
catch (MetaDataAccessException ex) {
logWarnIfNonZero(1, "Could not detect database type. Assuming locks can be taken.");
}
super.initialize(loadHelper, signaler);
} }
@Override @Override

View File

@ -28,6 +28,8 @@ import org.jspecify.annotations.Nullable;
import org.quartz.Scheduler; import org.quartz.Scheduler;
import org.quartz.SchedulerException; import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory; import org.quartz.SchedulerFactory;
import org.quartz.core.QuartzScheduler;
import org.quartz.core.QuartzSchedulerResources;
import org.quartz.impl.RemoteScheduler; import org.quartz.impl.RemoteScheduler;
import org.quartz.impl.SchedulerRepository; import org.quartz.impl.SchedulerRepository;
import org.quartz.impl.StdSchedulerFactory; import org.quartz.impl.StdSchedulerFactory;
@ -165,7 +167,7 @@ public class SchedulerFactoryBean extends SchedulerAccessor implements FactoryBe
private @Nullable SchedulerFactory schedulerFactory; private @Nullable SchedulerFactory schedulerFactory;
private Class<? extends SchedulerFactory> schedulerFactoryClass = StdSchedulerFactory.class; private Class<? extends SchedulerFactory> schedulerFactoryClass = LocalSchedulerFactory.class;
private @Nullable String schedulerName; private @Nullable String schedulerName;
@ -203,6 +205,8 @@ public class SchedulerFactoryBean extends SchedulerAccessor implements FactoryBe
private @Nullable Scheduler scheduler; private @Nullable Scheduler scheduler;
private @Nullable LocalDataSourceJobStore jobStore;
/** /**
* Set an external Quartz {@link SchedulerFactory} instance to use. * Set an external Quartz {@link SchedulerFactory} instance to use.
@ -223,11 +227,12 @@ public class SchedulerFactoryBean extends SchedulerAccessor implements FactoryBe
/** /**
* Set the Quartz {@link SchedulerFactory} implementation to use. * Set the Quartz {@link SchedulerFactory} implementation to use.
* <p>Default is the {@link StdSchedulerFactory} class, reading in the standard * <p>Default is a Spring-internal subclass of the {@link StdSchedulerFactory}
* {@code quartz.properties} from {@code quartz.jar}. For applying custom Quartz * class, reading in the standard {@code quartz.properties} from
* properties, specify {@link #setConfigLocation "configLocation"} and/or * {@code quartz.jar}. For applying custom Quartz properties,
* {@link #setQuartzProperties "quartzProperties"} etc on this local * specify {@link #setConfigLocation "configLocation"} and/or
* {@code SchedulerFactoryBean} instance. * {@link #setQuartzProperties "quartzProperties"} etc on this
* local {@code SchedulerFactoryBean} instance.
* @see org.quartz.impl.StdSchedulerFactory * @see org.quartz.impl.StdSchedulerFactory
* @see #setConfigLocation * @see #setConfigLocation
* @see #setQuartzProperties * @see #setQuartzProperties
@ -508,8 +513,9 @@ public class SchedulerFactoryBean extends SchedulerAccessor implements FactoryBe
private SchedulerFactory prepareSchedulerFactory() throws SchedulerException, IOException { private SchedulerFactory prepareSchedulerFactory() throws SchedulerException, IOException {
SchedulerFactory schedulerFactory = this.schedulerFactory; SchedulerFactory schedulerFactory = this.schedulerFactory;
if (schedulerFactory == null) { if (schedulerFactory == null) {
// Create local SchedulerFactory instance (typically a StdSchedulerFactory) // Create local SchedulerFactory instance (typically a LocalSchedulerFactory)
schedulerFactory = BeanUtils.instantiateClass(this.schedulerFactoryClass); schedulerFactory = (this.schedulerFactoryClass == LocalSchedulerFactory.class ?
new LocalSchedulerFactory() : BeanUtils.instantiateClass(this.schedulerFactoryClass));
if (schedulerFactory instanceof StdSchedulerFactory stdSchedulerFactory) { if (schedulerFactory instanceof StdSchedulerFactory stdSchedulerFactory) {
initSchedulerFactory(stdSchedulerFactory); initSchedulerFactory(stdSchedulerFactory);
} }
@ -778,6 +784,9 @@ public class SchedulerFactoryBean extends SchedulerAccessor implements FactoryBe
@Override @Override
public void start() throws SchedulingException { public void start() throws SchedulingException {
if (this.scheduler != null) { if (this.scheduler != null) {
if (this.jobStore != null) {
this.jobStore.initializeConnectionProvider();
}
try { try {
startScheduler(this.scheduler, this.startupDelay); startScheduler(this.scheduler, this.startupDelay);
} }
@ -829,4 +838,16 @@ public class SchedulerFactoryBean extends SchedulerAccessor implements FactoryBe
} }
} }
private class LocalSchedulerFactory extends StdSchedulerFactory {
@Override
protected Scheduler instantiate(QuartzSchedulerResources rsrcs, QuartzScheduler qs) {
if (rsrcs.getJobStore() instanceof LocalDataSourceJobStore ldsjs) {
SchedulerFactoryBean.this.jobStore = ldsjs;
}
return super.instantiate(rsrcs, qs);
}
}
} }

View File

@ -391,6 +391,8 @@ class QuartzSupportTests {
try (ClassPathXmlApplicationContext ctx = context("databasePersistence.xml")) { try (ClassPathXmlApplicationContext ctx = context("databasePersistence.xml")) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(ctx.getBean(DataSource.class)); JdbcTemplate jdbcTemplate = new JdbcTemplate(ctx.getBean(DataSource.class));
assertThat(jdbcTemplate.queryForList("SELECT * FROM qrtz_triggers").isEmpty()).as("No triggers were persisted").isFalse(); assertThat(jdbcTemplate.queryForList("SELECT * FROM qrtz_triggers").isEmpty()).as("No triggers were persisted").isFalse();
ctx.stop();
ctx.restart();
} }
} }

View File

@ -5,28 +5,28 @@
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> <bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers" ref="trigger" /> <property name="triggers" ref="trigger"/>
<property name="dataSource" ref="dataSource" /> <property name="dataSource" ref="dataSource"/>
</bean> </bean>
<bean id="trigger" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean"> <bean id="trigger" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">
<property name="repeatInterval" value="1000" /> <property name="repeatInterval" value="1000"/>
<property name="repeatCount" value="1" /> <property name="repeatCount" value="1"/>
<property name="jobDetail"> <property name="jobDetail">
<bean class="org.springframework.scheduling.quartz.JobDetailFactoryBean"> <bean class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
<property name="jobDataAsMap"> <property name="jobDataAsMap">
<map> <map>
<entry key="param" value="10" /> <entry key="param" value="10"/>
</map> </map>
</property> </property>
<property name="jobClass" value="org.springframework.scheduling.quartz.QuartzSupportTests$DummyJob" /> <property name="jobClass" value="org.springframework.scheduling.quartz.QuartzSupportTests$DummyJob"/>
<property name="durability" value="true" /> <property name="durability" value="true"/>
</bean> </bean>
</property> </property>
</bean> </bean>
<jdbc:embedded-database id="dataSource" type="HSQL"> <jdbc:embedded-database id="dataSource" type="HSQL">
<jdbc:script location="org/springframework/scheduling/quartz/quartz-hsql.sql" /> <jdbc:script location="org/springframework/scheduling/quartz/quartz-hsql.sql"/>
</jdbc:embedded-database> </jdbc:embedded-database>
</beans> </beans>