优化 job 调度:

1. 新增或修改 job 配置时,校验 handlerName 对应的 Spring Bean 存在
2. 删除 job 时,额外暂停 Trigger、取消调度,更完善
This commit is contained in:
YunaiV 2024-04-18 22:19:26 +08:00
parent 62c6c4b9bd
commit 1274f92544
4 changed files with 76 additions and 36 deletions

View File

@ -48,7 +48,7 @@ public class SchedulerManager {
.withIdentity(jobHandlerName).build(); .withIdentity(jobHandlerName).build();
// 创建 Trigger 对象 // 创建 Trigger 对象
Trigger trigger = this.buildTrigger(jobHandlerName, jobHandlerParam, cronExpression, retryCount, retryInterval); Trigger trigger = this.buildTrigger(jobHandlerName, jobHandlerParam, cronExpression, retryCount, retryInterval);
// 新增调度 // 新增 Job 调度
scheduler.scheduleJob(jobDetail, trigger); scheduler.scheduleJob(jobDetail, trigger);
} }
@ -80,6 +80,10 @@ public class SchedulerManager {
*/ */
public void deleteJob(String jobHandlerName) throws SchedulerException { public void deleteJob(String jobHandlerName) throws SchedulerException {
validateScheduler(); validateScheduler();
// 暂停 Trigger 对象
scheduler.pauseTrigger(new TriggerKey(jobHandlerName));
// 取消并删除 Job 调度
scheduler.unscheduleJob(new TriggerKey(jobHandlerName));
scheduler.deleteJob(new JobKey(jobHandlerName)); scheduler.deleteJob(new JobKey(jobHandlerName));
} }

View File

@ -22,6 +22,8 @@ public interface ErrorCodeConstants {
ErrorCode JOB_CHANGE_STATUS_EQUALS = new ErrorCode(1_001_001_003, "定时任务已经处于该状态,无需修改"); ErrorCode JOB_CHANGE_STATUS_EQUALS = new ErrorCode(1_001_001_003, "定时任务已经处于该状态,无需修改");
ErrorCode JOB_UPDATE_ONLY_NORMAL_STATUS = new ErrorCode(1_001_001_004, "只有开启状态的任务,才可以修改"); ErrorCode JOB_UPDATE_ONLY_NORMAL_STATUS = new ErrorCode(1_001_001_004, "只有开启状态的任务,才可以修改");
ErrorCode JOB_CRON_EXPRESSION_VALID = new ErrorCode(1_001_001_005, "CRON 表达式不正确"); ErrorCode JOB_CRON_EXPRESSION_VALID = new ErrorCode(1_001_001_005, "CRON 表达式不正确");
ErrorCode JOB_HANDLER_BEAN_NOT_EXISTS = new ErrorCode(1_001_001_006, "定时任务的处理器 Bean 不存在");
ErrorCode JOB_HANDLER_BEAN_TYPE_ERROR = new ErrorCode(1_001_001_007, "定时任务的处理器 Bean 类型不正确,未实现 JobHandler 接口");
// ========== API 错误日志 1-001-002-000 ========== // ========== API 错误日志 1-001-002-000 ==========
ErrorCode API_ERROR_LOG_NOT_FOUND = new ErrorCode(1_001_002_000, "API 错误日志不存在"); ErrorCode API_ERROR_LOG_NOT_FOUND = new ErrorCode(1_001_002_000, "API 错误日志不存在");

View File

@ -1,7 +1,9 @@
package cn.iocoder.yudao.module.infra.service.job; package cn.iocoder.yudao.module.infra.service.job;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
import cn.iocoder.yudao.framework.quartz.core.scheduler.SchedulerManager; import cn.iocoder.yudao.framework.quartz.core.scheduler.SchedulerManager;
import cn.iocoder.yudao.framework.quartz.core.util.CronUtils; import cn.iocoder.yudao.framework.quartz.core.util.CronUtils;
import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobPageReqVO; import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobPageReqVO;
@ -9,13 +11,12 @@ import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobSaveReqVO;
import cn.iocoder.yudao.module.infra.dal.dataobject.job.JobDO; import cn.iocoder.yudao.module.infra.dal.dataobject.job.JobDO;
import cn.iocoder.yudao.module.infra.dal.mysql.job.JobMapper; import cn.iocoder.yudao.module.infra.dal.mysql.job.JobMapper;
import cn.iocoder.yudao.module.infra.enums.job.JobStatusEnum; import cn.iocoder.yudao.module.infra.enums.job.JobStatusEnum;
import jakarta.annotation.Resource;
import org.quartz.SchedulerException; import org.quartz.SchedulerException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import jakarta.annotation.Resource;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.containsAny; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.containsAny;
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*; import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
@ -39,24 +40,25 @@ public class JobServiceImpl implements JobService {
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public Long createJob(JobSaveReqVO createReqVO) throws SchedulerException { public Long createJob(JobSaveReqVO createReqVO) throws SchedulerException {
validateCronExpression(createReqVO.getCronExpression()); validateCronExpression(createReqVO.getCronExpression());
// 校验唯一性 // 1.1 校验唯一性
if (jobMapper.selectByHandlerName(createReqVO.getHandlerName()) != null) { if (jobMapper.selectByHandlerName(createReqVO.getHandlerName()) != null) {
throw exception(JOB_HANDLER_EXISTS); throw exception(JOB_HANDLER_EXISTS);
} }
// 插入 // 1.2 校验 JobHandler 是否存在
validateJobHandlerExists(createReqVO.getHandlerName());
// 2. 插入 JobDO
JobDO job = BeanUtils.toBean(createReqVO, JobDO.class); JobDO job = BeanUtils.toBean(createReqVO, JobDO.class);
job.setStatus(JobStatusEnum.INIT.getStatus()); job.setStatus(JobStatusEnum.INIT.getStatus());
fillJobMonitorTimeoutEmpty(job); fillJobMonitorTimeoutEmpty(job);
jobMapper.insert(job); jobMapper.insert(job);
// 添加 Job Quartz // 3.1 添加 Job Quartz
schedulerManager.addJob(job.getId(), job.getHandlerName(), job.getHandlerParam(), job.getCronExpression(), schedulerManager.addJob(job.getId(), job.getHandlerName(), job.getHandlerParam(), job.getCronExpression(),
createReqVO.getRetryCount(), createReqVO.getRetryInterval()); createReqVO.getRetryCount(), createReqVO.getRetryInterval());
// 更新 // 3.2 更新 JobDO
JobDO updateObj = JobDO.builder().id(job.getId()).status(JobStatusEnum.NORMAL.getStatus()).build(); JobDO updateObj = JobDO.builder().id(job.getId()).status(JobStatusEnum.NORMAL.getStatus()).build();
jobMapper.updateById(updateObj); jobMapper.updateById(updateObj);
// 返回
return job.getId(); return job.getId();
} }
@ -64,22 +66,35 @@ public class JobServiceImpl implements JobService {
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void updateJob(JobSaveReqVO updateReqVO) throws SchedulerException { public void updateJob(JobSaveReqVO updateReqVO) throws SchedulerException {
validateCronExpression(updateReqVO.getCronExpression()); validateCronExpression(updateReqVO.getCronExpression());
// 校验存在 // 1.1 校验存在
JobDO job = validateJobExists(updateReqVO.getId()); JobDO job = validateJobExists(updateReqVO.getId());
// 只有开启状态才可以修改.原因是如果出暂停状态修改 Quartz Job 会导致任务又开始执行 // 1.2 只有开启状态才可以修改.原因是如果出暂停状态修改 Quartz Job 会导致任务又开始执行
if (!job.getStatus().equals(JobStatusEnum.NORMAL.getStatus())) { if (!job.getStatus().equals(JobStatusEnum.NORMAL.getStatus())) {
throw exception(JOB_UPDATE_ONLY_NORMAL_STATUS); throw exception(JOB_UPDATE_ONLY_NORMAL_STATUS);
} }
// 更新 // 1.3 校验 JobHandler 是否存在
validateJobHandlerExists(updateReqVO.getHandlerName());
// 2. 更新 JobDO
JobDO updateObj = BeanUtils.toBean(updateReqVO, JobDO.class); JobDO updateObj = BeanUtils.toBean(updateReqVO, JobDO.class);
fillJobMonitorTimeoutEmpty(updateObj); fillJobMonitorTimeoutEmpty(updateObj);
jobMapper.updateById(updateObj); jobMapper.updateById(updateObj);
// 更新 Job Quartz // 3. 更新 Job Quartz
schedulerManager.updateJob(job.getHandlerName(), updateReqVO.getHandlerParam(), updateReqVO.getCronExpression(), schedulerManager.updateJob(job.getHandlerName(), updateReqVO.getHandlerParam(), updateReqVO.getCronExpression(),
updateReqVO.getRetryCount(), updateReqVO.getRetryInterval()); updateReqVO.getRetryCount(), updateReqVO.getRetryInterval());
} }
private void validateJobHandlerExists(String handlerName) {
Object handler = SpringUtil.getBean(handlerName);
if (handler == null) {
throw exception(JOB_HANDLER_BEAN_NOT_EXISTS);
}
if (!(handler instanceof JobHandler)) {
throw exception(JOB_HANDLER_BEAN_TYPE_ERROR);
}
}
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void updateJobStatus(Long id, Integer status) throws SchedulerException { public void updateJobStatus(Long id, Integer status) throws SchedulerException {

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.infra.service.job; package cn.iocoder.yudao.module.infra.service.job;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.quartz.core.scheduler.SchedulerManager; import cn.iocoder.yudao.framework.quartz.core.scheduler.SchedulerManager;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
@ -8,7 +9,9 @@ import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobSaveReqVO;
import cn.iocoder.yudao.module.infra.dal.dataobject.job.JobDO; import cn.iocoder.yudao.module.infra.dal.dataobject.job.JobDO;
import cn.iocoder.yudao.module.infra.dal.mysql.job.JobMapper; import cn.iocoder.yudao.module.infra.dal.mysql.job.JobMapper;
import cn.iocoder.yudao.module.infra.enums.job.JobStatusEnum; import cn.iocoder.yudao.module.infra.enums.job.JobStatusEnum;
import cn.iocoder.yudao.module.infra.job.job.JobLogCleanJob;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.quartz.SchedulerException; import org.quartz.SchedulerException;
import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
@ -23,6 +26,7 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*; import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@Import(JobServiceImpl.class) @Import(JobServiceImpl.class)
@ -35,6 +39,9 @@ public class JobServiceImplTest extends BaseDbUnitTest {
@MockBean @MockBean
private SchedulerManager schedulerManager; private SchedulerManager schedulerManager;
@MockBean
private JobLogCleanJob jobLogCleanJob;
@Test @Test
public void testCreateJob_cronExpressionValid() { public void testCreateJob_cronExpressionValid() {
// 准备参数Cron 表达式为 String 类型默认随机字符串 // 准备参数Cron 表达式为 String 类型默认随机字符串
@ -48,11 +55,15 @@ public class JobServiceImplTest extends BaseDbUnitTest {
public void testCreateJob_jobHandlerExists() throws SchedulerException { public void testCreateJob_jobHandlerExists() throws SchedulerException {
// 准备参数 指定 Cron 表达式 // 准备参数 指定 Cron 表达式
JobSaveReqVO reqVO = randomPojo(JobSaveReqVO.class, o -> o.setCronExpression("0 0/1 * * * ? *")); JobSaveReqVO reqVO = randomPojo(JobSaveReqVO.class, o -> o.setCronExpression("0 0/1 * * * ? *"));
try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(reqVO.getHandlerName())))
.thenReturn(jobLogCleanJob);
// 调用 // 调用
jobService.createJob(reqVO); jobService.createJob(reqVO);
// 调用并断言异常 // 调用并断言异常
assertServiceException(() -> jobService.createJob(reqVO), JOB_HANDLER_EXISTS); assertServiceException(() -> jobService.createJob(reqVO), JOB_HANDLER_EXISTS);
}
} }
@Test @Test
@ -60,18 +71,22 @@ public class JobServiceImplTest extends BaseDbUnitTest {
// 准备参数 指定 Cron 表达式 // 准备参数 指定 Cron 表达式
JobSaveReqVO reqVO = randomPojo(JobSaveReqVO.class, o -> o.setCronExpression("0 0/1 * * * ? *")) JobSaveReqVO reqVO = randomPojo(JobSaveReqVO.class, o -> o.setCronExpression("0 0/1 * * * ? *"))
.setId(null); .setId(null);
try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(reqVO.getHandlerName())))
.thenReturn(jobLogCleanJob);
// 调用 // 调用
Long jobId = jobService.createJob(reqVO); Long jobId = jobService.createJob(reqVO);
// 断言 // 断言
assertNotNull(jobId); assertNotNull(jobId);
// 校验记录的属性是否正确 // 校验记录的属性是否正确
JobDO job = jobMapper.selectById(jobId); JobDO job = jobMapper.selectById(jobId);
assertPojoEquals(reqVO, job, "id"); assertPojoEquals(reqVO, job, "id");
assertEquals(JobStatusEnum.NORMAL.getStatus(), job.getStatus()); assertEquals(JobStatusEnum.NORMAL.getStatus(), job.getStatus());
// 校验调用 // 校验调用
verify(schedulerManager).addJob(eq(job.getId()), eq(job.getHandlerName()), eq(job.getHandlerParam()), verify(schedulerManager).addJob(eq(job.getId()), eq(job.getHandlerName()), eq(job.getHandlerParam()),
eq(job.getCronExpression()), eq(reqVO.getRetryCount()), eq(reqVO.getRetryInterval())); eq(job.getCronExpression()), eq(reqVO.getRetryCount()), eq(reqVO.getRetryInterval()));
}
} }
@Test @Test
@ -109,15 +124,19 @@ public class JobServiceImplTest extends BaseDbUnitTest {
o.setId(job.getId()); o.setId(job.getId());
o.setCronExpression("0 0/1 * * * ? *"); o.setCronExpression("0 0/1 * * * ? *");
}); });
try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(updateReqVO.getHandlerName())))
.thenReturn(jobLogCleanJob);
// 调用 // 调用
jobService.updateJob(updateReqVO); jobService.updateJob(updateReqVO);
// 校验记录的属性是否正确 // 校验记录的属性是否正确
JobDO updateJob = jobMapper.selectById(updateReqVO.getId()); JobDO updateJob = jobMapper.selectById(updateReqVO.getId());
assertPojoEquals(updateReqVO, updateJob); assertPojoEquals(updateReqVO, updateJob);
// 校验调用 // 校验调用
verify(schedulerManager).updateJob(eq(job.getHandlerName()), eq(updateReqVO.getHandlerParam()), verify(schedulerManager).updateJob(eq(job.getHandlerName()), eq(updateReqVO.getHandlerParam()),
eq(updateReqVO.getCronExpression()), eq(updateReqVO.getRetryCount()), eq(updateReqVO.getRetryInterval())); eq(updateReqVO.getCronExpression()), eq(updateReqVO.getRetryCount()), eq(updateReqVO.getRetryInterval()));
}
} }
@Test @Test