feat: 新增嵌入式分布式调度底座
- 提供通用调度 API、Quartz JDBC Provider 与独立 Starter - 补充 MySQL、PostgreSQL、H2 建表脚本与接入校验 - 同步完善 Federation 与 Scheduler 模块说明
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
package com.easyagents.scheduler.spring.boot;
|
||||
|
||||
import com.easyagents.scheduler.ScheduleErrorCode;
|
||||
import com.easyagents.scheduler.ScheduleException;
|
||||
import com.easyagents.scheduler.ScheduleExecutionListener;
|
||||
import com.easyagents.scheduler.ScheduleHandler;
|
||||
import com.easyagents.scheduler.ScheduleService;
|
||||
import com.easyagents.scheduler.quartz.QuartzScheduleService;
|
||||
import com.easyagents.scheduler.quartz.QuartzSchedulerConfig;
|
||||
import com.easyagents.scheduler.quartz.QuartzSchedulerFactory;
|
||||
import org.quartz.impl.StdSchedulerFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.beans.factory.ListableBeanFactory;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Easy Agents 独立调度器自动配置。
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
|
||||
@ConditionalOnClass({ScheduleService.class, StdSchedulerFactory.class, DataSource.class})
|
||||
@ConditionalOnProperty(
|
||||
prefix = "easy-agents.scheduler",
|
||||
name = "enabled",
|
||||
havingValue = "true"
|
||||
)
|
||||
@EnableConfigurationProperties(EasyAgentsSchedulerProperties.class)
|
||||
public class EasyAgentsSchedulerAutoConfiguration {
|
||||
|
||||
/** Easy Agents Quartz Scheduler 的明确 Bean 名称。 */
|
||||
public static final String SCHEDULER_BEAN_NAME = "easyAgentsQuartzScheduleService";
|
||||
|
||||
/**
|
||||
* 创建调度器自动配置。
|
||||
*/
|
||||
public EasyAgentsSchedulerAutoConfiguration() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建与调用方其他 Quartz Scheduler 隔离的调度服务。
|
||||
*
|
||||
* @param properties 调度器配置
|
||||
* @param beanFactory 用于明确选择 DataSource
|
||||
* @param handlers 调度 Handler Bean
|
||||
* @param listeners 调度监听器 Bean
|
||||
* @return 尚未启动、由 Spring 生命周期启动的调度服务
|
||||
* @throws ScheduleException DataSource 选择或 Provider 初始化失败时抛出
|
||||
*/
|
||||
@Bean(name = SCHEDULER_BEAN_NAME, initMethod = "start", destroyMethod = "close")
|
||||
@ConditionalOnMissingBean(ScheduleService.class)
|
||||
public QuartzScheduleService easyAgentsQuartzScheduleService(
|
||||
EasyAgentsSchedulerProperties properties,
|
||||
ListableBeanFactory beanFactory,
|
||||
ObjectProvider<ScheduleHandler> handlers,
|
||||
ObjectProvider<ScheduleExecutionListener> listeners
|
||||
) {
|
||||
DataSource dataSource = selectDataSource(
|
||||
beanFactory,
|
||||
properties.getDataSourceBeanName()
|
||||
);
|
||||
EasyAgentsSchedulerProperties.Quartz quartz = properties.getQuartz();
|
||||
if (quartz == null) {
|
||||
throw providerFailure("easy-agents.scheduler.quartz must not be null");
|
||||
}
|
||||
QuartzSchedulerConfig config = new QuartzSchedulerConfig(
|
||||
quartz.getSchedulerName(),
|
||||
quartz.getInstanceId(),
|
||||
quartz.getTablePrefix(),
|
||||
quartz.getDriverDelegateClass(),
|
||||
quartz.isClustered(),
|
||||
quartz.getThreadCount(),
|
||||
quartz.getThreadPriority(),
|
||||
quartz.getClusterCheckinIntervalMillis(),
|
||||
quartz.getMisfireThresholdMillis(),
|
||||
quartz.isWaitForJobsToCompleteOnShutdown(),
|
||||
quartz.getShutdownWaitTimeoutMillis(),
|
||||
quartz.isValidateSchema()
|
||||
);
|
||||
return QuartzSchedulerFactory.createJdbc(
|
||||
dataSource,
|
||||
config,
|
||||
handlers.orderedStream().toList(),
|
||||
listeners.orderedStream().toList()
|
||||
);
|
||||
}
|
||||
|
||||
private static DataSource selectDataSource(
|
||||
ListableBeanFactory beanFactory,
|
||||
String requestedBeanName
|
||||
) {
|
||||
if (requestedBeanName != null && !requestedBeanName.isBlank()) {
|
||||
String normalizedName = requestedBeanName.trim();
|
||||
try {
|
||||
return beanFactory.getBean(normalizedName, DataSource.class);
|
||||
} catch (RuntimeException exception) {
|
||||
throw new ScheduleException(
|
||||
ScheduleErrorCode.PROVIDER_FAILURE,
|
||||
"configured scheduler DataSource bean is unavailable: " + normalizedName,
|
||||
exception
|
||||
);
|
||||
}
|
||||
}
|
||||
Map<String, DataSource> candidates = BeanFactoryUtils.beansOfTypeIncludingAncestors(
|
||||
beanFactory,
|
||||
DataSource.class,
|
||||
true,
|
||||
false
|
||||
);
|
||||
Collection<DataSource> uniqueCandidates = candidates.values().stream().distinct().toList();
|
||||
if (uniqueCandidates.size() == 1) {
|
||||
return uniqueCandidates.iterator().next();
|
||||
}
|
||||
String names = Arrays.toString(candidates.keySet().toArray(String[]::new));
|
||||
if (uniqueCandidates.isEmpty()) {
|
||||
throw providerFailure(
|
||||
"scheduler is enabled but no DataSource bean exists; configure "
|
||||
+ "easy-agents.scheduler.data-source-bean-name or provide one DataSource"
|
||||
);
|
||||
}
|
||||
throw providerFailure(
|
||||
"multiple DataSource beans found " + names + "; configure "
|
||||
+ "easy-agents.scheduler.data-source-bean-name"
|
||||
);
|
||||
}
|
||||
|
||||
private static ScheduleException providerFailure(String message) {
|
||||
return new ScheduleException(ScheduleErrorCode.PROVIDER_FAILURE, message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
package com.easyagents.scheduler.spring.boot;
|
||||
|
||||
import com.easyagents.scheduler.quartz.QuartzSchedulerConfig;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* Easy Agents 嵌入式调度器配置。
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "easy-agents.scheduler")
|
||||
public class EasyAgentsSchedulerProperties {
|
||||
|
||||
/** 是否显式启用调度器。 */
|
||||
private boolean enabled;
|
||||
|
||||
/** 多 DataSource 场景下选用的 Bean 名称。 */
|
||||
private String dataSourceBeanName;
|
||||
|
||||
/** Quartz Provider 配置。 */
|
||||
private Quartz quartz = new Quartz();
|
||||
|
||||
/**
|
||||
* 创建默认调度器配置。
|
||||
*/
|
||||
public EasyAgentsSchedulerProperties() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回是否启用调度器。
|
||||
*
|
||||
* @return 启用状态
|
||||
*/
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否启用调度器。
|
||||
*
|
||||
* @param enabled 启用状态
|
||||
*/
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回指定 DataSource Bean 名称。
|
||||
*
|
||||
* @return Bean 名称,可为空
|
||||
*/
|
||||
public String getDataSourceBeanName() {
|
||||
return dataSourceBeanName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置指定 DataSource Bean 名称。
|
||||
*
|
||||
* @param dataSourceBeanName Bean 名称
|
||||
*/
|
||||
public void setDataSourceBeanName(String dataSourceBeanName) {
|
||||
this.dataSourceBeanName = dataSourceBeanName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 Quartz Provider 配置。
|
||||
*
|
||||
* @return Quartz 配置
|
||||
*/
|
||||
public Quartz getQuartz() {
|
||||
return quartz;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 Quartz Provider 配置。
|
||||
*
|
||||
* @param quartz Quartz 配置
|
||||
*/
|
||||
public void setQuartz(Quartz quartz) {
|
||||
this.quartz = quartz;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quartz Provider 属性集合。
|
||||
*/
|
||||
public static class Quartz {
|
||||
|
||||
/** Scheduler Name,同一集群保持一致。 */
|
||||
private String schedulerName = "easyAgentsScheduler";
|
||||
|
||||
/** 当前实例标识。 */
|
||||
private String instanceId = "AUTO";
|
||||
|
||||
/** Quartz 表前缀,可包含 Schema。 */
|
||||
private String tablePrefix = "QRTZ_";
|
||||
|
||||
/** Quartz JDBC DriverDelegate 类名。 */
|
||||
private String driverDelegateClass = QuartzSchedulerConfig.STANDARD_JDBC_DELEGATE;
|
||||
|
||||
/** 是否启用 JDBC 集群。 */
|
||||
private boolean clustered = true;
|
||||
|
||||
/** Quartz Worker 线程数。 */
|
||||
private int threadCount = 8;
|
||||
|
||||
/** Quartz Worker 线程优先级。 */
|
||||
private int threadPriority = Thread.NORM_PRIORITY;
|
||||
|
||||
/** 集群心跳间隔,单位毫秒。 */
|
||||
private long clusterCheckinIntervalMillis = 15_000L;
|
||||
|
||||
/** Misfire 判定阈值,单位毫秒。 */
|
||||
private long misfireThresholdMillis = 60_000L;
|
||||
|
||||
/** 关闭时是否等待运行中任务完成。 */
|
||||
private boolean waitForJobsToCompleteOnShutdown = true;
|
||||
|
||||
/** 关闭等待运行中任务的最长时间,单位毫秒。 */
|
||||
private long shutdownWaitTimeoutMillis = 30_000L;
|
||||
|
||||
/** 启动前是否只读校验 Quartz 表结构。 */
|
||||
private boolean validateSchema = true;
|
||||
|
||||
/**
|
||||
* 创建默认 Quartz Provider 配置。
|
||||
*/
|
||||
public Quartz() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 Scheduler Name。
|
||||
*
|
||||
* @return Scheduler Name
|
||||
*/
|
||||
public String getSchedulerName() {
|
||||
return schedulerName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 Scheduler Name。
|
||||
*
|
||||
* @param schedulerName Scheduler Name
|
||||
*/
|
||||
public void setSchedulerName(String schedulerName) {
|
||||
this.schedulerName = schedulerName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回实例标识。
|
||||
*
|
||||
* @return 实例标识
|
||||
*/
|
||||
public String getInstanceId() {
|
||||
return instanceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置实例标识。
|
||||
*
|
||||
* @param instanceId 实例标识
|
||||
*/
|
||||
public void setInstanceId(String instanceId) {
|
||||
this.instanceId = instanceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回表前缀。
|
||||
*
|
||||
* @return 表前缀
|
||||
*/
|
||||
public String getTablePrefix() {
|
||||
return tablePrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置表前缀。
|
||||
*
|
||||
* @param tablePrefix 表前缀
|
||||
*/
|
||||
public void setTablePrefix(String tablePrefix) {
|
||||
this.tablePrefix = tablePrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 JDBC Delegate 类名。
|
||||
*
|
||||
* @return Delegate 类名
|
||||
*/
|
||||
public String getDriverDelegateClass() {
|
||||
return driverDelegateClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 JDBC Delegate 类名。
|
||||
*
|
||||
* @param driverDelegateClass Delegate 类名
|
||||
*/
|
||||
public void setDriverDelegateClass(String driverDelegateClass) {
|
||||
this.driverDelegateClass = driverDelegateClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回是否启用集群。
|
||||
*
|
||||
* @return 集群状态
|
||||
*/
|
||||
public boolean isClustered() {
|
||||
return clustered;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否启用集群。
|
||||
*
|
||||
* @param clustered 集群状态
|
||||
*/
|
||||
public void setClustered(boolean clustered) {
|
||||
this.clustered = clustered;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 Worker 线程数。
|
||||
*
|
||||
* @return 线程数
|
||||
*/
|
||||
public int getThreadCount() {
|
||||
return threadCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 Worker 线程数。
|
||||
*
|
||||
* @param threadCount 线程数
|
||||
*/
|
||||
public void setThreadCount(int threadCount) {
|
||||
this.threadCount = threadCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 Worker 线程优先级。
|
||||
*
|
||||
* @return 线程优先级
|
||||
*/
|
||||
public int getThreadPriority() {
|
||||
return threadPriority;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 Worker 线程优先级。
|
||||
*
|
||||
* @param threadPriority 线程优先级
|
||||
*/
|
||||
public void setThreadPriority(int threadPriority) {
|
||||
this.threadPriority = threadPriority;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回集群心跳间隔。
|
||||
*
|
||||
* @return 毫秒间隔
|
||||
*/
|
||||
public long getClusterCheckinIntervalMillis() {
|
||||
return clusterCheckinIntervalMillis;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置集群心跳间隔。
|
||||
*
|
||||
* @param clusterCheckinIntervalMillis 毫秒间隔
|
||||
*/
|
||||
public void setClusterCheckinIntervalMillis(long clusterCheckinIntervalMillis) {
|
||||
this.clusterCheckinIntervalMillis = clusterCheckinIntervalMillis;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 Misfire 判定阈值。
|
||||
*
|
||||
* @return 毫秒阈值
|
||||
*/
|
||||
public long getMisfireThresholdMillis() {
|
||||
return misfireThresholdMillis;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 Misfire 判定阈值。
|
||||
*
|
||||
* @param misfireThresholdMillis 毫秒阈值
|
||||
*/
|
||||
public void setMisfireThresholdMillis(long misfireThresholdMillis) {
|
||||
this.misfireThresholdMillis = misfireThresholdMillis;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回关闭时是否等待在途任务。
|
||||
*
|
||||
* @return 等待状态
|
||||
*/
|
||||
public boolean isWaitForJobsToCompleteOnShutdown() {
|
||||
return waitForJobsToCompleteOnShutdown;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置关闭时是否等待在途任务。
|
||||
*
|
||||
* @param waitForJobsToCompleteOnShutdown 等待状态
|
||||
*/
|
||||
public void setWaitForJobsToCompleteOnShutdown(
|
||||
boolean waitForJobsToCompleteOnShutdown
|
||||
) {
|
||||
this.waitForJobsToCompleteOnShutdown = waitForJobsToCompleteOnShutdown;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回关闭等待的最长时间。
|
||||
*
|
||||
* @return 毫秒数
|
||||
*/
|
||||
public long getShutdownWaitTimeoutMillis() {
|
||||
return shutdownWaitTimeoutMillis;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置关闭等待的最长时间。
|
||||
*
|
||||
* @param shutdownWaitTimeoutMillis 毫秒数
|
||||
*/
|
||||
public void setShutdownWaitTimeoutMillis(long shutdownWaitTimeoutMillis) {
|
||||
this.shutdownWaitTimeoutMillis = shutdownWaitTimeoutMillis;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回是否校验数据库结构。
|
||||
*
|
||||
* @return 校验状态
|
||||
*/
|
||||
public boolean isValidateSchema() {
|
||||
return validateSchema;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否校验数据库结构。
|
||||
*
|
||||
* @param validateSchema 校验状态
|
||||
*/
|
||||
public void setValidateSchema(boolean validateSchema) {
|
||||
this.validateSchema = validateSchema;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
com.easyagents.scheduler.spring.boot.EasyAgentsSchedulerAutoConfiguration
|
||||
@@ -0,0 +1 @@
|
||||
com.easyagents.scheduler.spring.boot.EasyAgentsSchedulerAutoConfiguration
|
||||
@@ -0,0 +1,237 @@
|
||||
package com.easyagents.scheduler.spring.boot;
|
||||
|
||||
import com.easyagents.scheduler.ConcurrencyPolicy;
|
||||
import com.easyagents.scheduler.MisfirePolicy;
|
||||
import com.easyagents.scheduler.OnceSchedulePlan;
|
||||
import com.easyagents.scheduler.ScheduleDefinition;
|
||||
import com.easyagents.scheduler.ScheduleException;
|
||||
import com.easyagents.scheduler.ScheduleFireContext;
|
||||
import com.easyagents.scheduler.ScheduleHandler;
|
||||
import com.easyagents.scheduler.ScheduleId;
|
||||
import com.easyagents.scheduler.ScheduleService;
|
||||
import org.h2.jdbcx.JdbcDataSource;
|
||||
import org.h2.tools.RunScript;
|
||||
import org.junit.Test;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.core.env.MapPropertySource;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.Connection;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.Statement;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
/**
|
||||
* {@link EasyAgentsSchedulerAutoConfiguration} 的 Spring Boot 2.7/3.5 装配测试。
|
||||
*/
|
||||
public class EasyAgentsSchedulerAutoConfigurationTest {
|
||||
|
||||
/**
|
||||
* 验证默认关闭时不创建调度器,也不要求 DataSource。
|
||||
*/
|
||||
@Test
|
||||
public void shouldStayDisabledByDefault() {
|
||||
try (AnnotationConfigApplicationContext context = newContext(Map.of())) {
|
||||
assertFalse(context.containsBean(
|
||||
EasyAgentsSchedulerAutoConfiguration.SCHEDULER_BEAN_NAME
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证显式启用后完成属性绑定、Handler 收集、启动和关闭。
|
||||
*/
|
||||
@Test
|
||||
public void shouldConfigureAndStartScheduler() throws Exception {
|
||||
JdbcDataSource dataSource = dataSource();
|
||||
executeSchema(dataSource);
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
Map<String, Object> properties = enabledProperties();
|
||||
properties.put("easy-agents.scheduler.data-source-bean-name", "schedulerDataSource");
|
||||
|
||||
SpringApplication application = new SpringApplication(AutoDiscoveryApplication.class);
|
||||
application.setWebApplicationType(WebApplicationType.NONE);
|
||||
application.setDefaultProperties(properties);
|
||||
application.addInitializers(applicationContext -> {
|
||||
GenericApplicationContext genericContext =
|
||||
(GenericApplicationContext) applicationContext;
|
||||
genericContext.registerBean(
|
||||
"schedulerDataSource",
|
||||
DataSource.class,
|
||||
() -> dataSource
|
||||
);
|
||||
genericContext.registerBean(
|
||||
"unrelatedDataSource",
|
||||
DataSource.class,
|
||||
() -> dataSource()
|
||||
);
|
||||
genericContext.registerBean(
|
||||
"testScheduleHandler",
|
||||
ScheduleHandler.class,
|
||||
() -> handler(latch)
|
||||
);
|
||||
});
|
||||
ConfigurableApplicationContext context = application.run();
|
||||
try {
|
||||
ScheduleService service = context.getBean(ScheduleService.class);
|
||||
assertNotNull(service);
|
||||
ScheduleDefinition definition = definition();
|
||||
service.create(definition);
|
||||
service.triggerNow(definition.id(), "starter-invocation", Map.of());
|
||||
assertTrue("starter handler did not execute", latch.await(5, TimeUnit.SECONDS));
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
|
||||
// Spring 销毁 Scheduler 后,调用方持有的 DataSource 仍然可用。
|
||||
try (
|
||||
Connection connection = dataSource.getConnection();
|
||||
Statement statement = connection.createStatement();
|
||||
ResultSet resultSet = statement.executeQuery("SELECT COUNT(*) FROM QRTZ_JOB_DETAILS")
|
||||
) {
|
||||
assertTrue(resultSet.next());
|
||||
assertEquals(1, resultSet.getInt(1));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证多个 DataSource 未明确选择时启动失败并提供可操作信息。
|
||||
*/
|
||||
@Test
|
||||
public void shouldRejectAmbiguousDataSources() {
|
||||
AnnotationConfigApplicationContext context = context(enabledProperties());
|
||||
context.registerBean("firstDataSource", DataSource.class, () -> dataSource());
|
||||
context.registerBean("secondDataSource", DataSource.class, () -> dataSource());
|
||||
context.register(EasyAgentsSchedulerAutoConfiguration.class);
|
||||
try {
|
||||
context.refresh();
|
||||
fail("expected ambiguous DataSource failure");
|
||||
} catch (RuntimeException exception) {
|
||||
ScheduleException scheduleException = findCause(exception, ScheduleException.class);
|
||||
assertNotNull(scheduleException);
|
||||
assertTrue(scheduleException.getMessage().contains("data-source-bean-name"));
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static AnnotationConfigApplicationContext newContext(
|
||||
Map<String, Object> properties
|
||||
) {
|
||||
AnnotationConfigApplicationContext context = context(properties);
|
||||
context.register(EasyAgentsSchedulerAutoConfiguration.class);
|
||||
context.refresh();
|
||||
return context;
|
||||
}
|
||||
|
||||
private static AnnotationConfigApplicationContext context(Map<String, Object> properties) {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
context.getEnvironment().getPropertySources().addFirst(
|
||||
new MapPropertySource("scheduler-test", properties)
|
||||
);
|
||||
return context;
|
||||
}
|
||||
|
||||
private static Map<String, Object> enabledProperties() {
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
properties.put("easy-agents.scheduler.enabled", "true");
|
||||
properties.put(
|
||||
"easy-agents.scheduler.quartz.scheduler-name",
|
||||
"starter-scheduler-" + UUID.randomUUID()
|
||||
);
|
||||
properties.put("easy-agents.scheduler.quartz.instance-id", "NON_CLUSTERED");
|
||||
properties.put("easy-agents.scheduler.quartz.clustered", "false");
|
||||
properties.put("easy-agents.scheduler.quartz.thread-count", "2");
|
||||
properties.put("easy-agents.scheduler.quartz.misfire-threshold-millis", "1000");
|
||||
return properties;
|
||||
}
|
||||
|
||||
private static JdbcDataSource dataSource() {
|
||||
JdbcDataSource dataSource = new JdbcDataSource();
|
||||
dataSource.setURL("jdbc:h2:mem:starter-" + UUID.randomUUID() + ";DB_CLOSE_DELAY=-1");
|
||||
dataSource.setUser("sa");
|
||||
dataSource.setPassword("");
|
||||
return dataSource;
|
||||
}
|
||||
|
||||
private static void executeSchema(JdbcDataSource dataSource) throws Exception {
|
||||
InputStream stream = EasyAgentsSchedulerAutoConfigurationTest.class
|
||||
.getClassLoader()
|
||||
.getResourceAsStream("quartz-schema/h2-2.5.2.sql");
|
||||
if (stream == null) {
|
||||
throw new IllegalStateException("H2 Quartz schema resource not found");
|
||||
}
|
||||
try (
|
||||
Connection connection = dataSource.getConnection();
|
||||
InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)
|
||||
) {
|
||||
RunScript.execute(connection, reader);
|
||||
}
|
||||
}
|
||||
|
||||
private static ScheduleHandler handler(CountDownLatch latch) {
|
||||
return new ScheduleHandler() {
|
||||
@Override
|
||||
public String code() {
|
||||
return "starter-handler";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(ScheduleFireContext context) {
|
||||
latch.countDown();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static ScheduleDefinition definition() {
|
||||
return new ScheduleDefinition(
|
||||
new ScheduleId("starter", "trigger"),
|
||||
"starter-handler",
|
||||
new OnceSchedulePlan(Instant.parse("2099-01-01T00:00:00Z")),
|
||||
MisfirePolicy.FIRE_ONCE_NOW,
|
||||
ConcurrencyPolicy.DISALLOW,
|
||||
true,
|
||||
Map.of("source", "starter"),
|
||||
"starter test"
|
||||
);
|
||||
}
|
||||
|
||||
private static <T extends Throwable> T findCause(Throwable failure, Class<T> type) {
|
||||
Throwable current = failure;
|
||||
while (current != null) {
|
||||
if (type.isInstance(current)) {
|
||||
return type.cast(current);
|
||||
}
|
||||
current = current.getCause();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 只启用 Spring Boot 自动配置发现的测试应用。
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableAutoConfiguration
|
||||
static class AutoDiscoveryApplication {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user