背景
elastic-job 的顶层对象 JobScheduler 提供了 shutdown 方法退出定时任务,但该 API 只会清理内部组件的资源,并没有通知内部运行的定时任务进行退出。
如果能够以线程中断的形式通知定时任务退出,那么用户可以自定义自己的退出逻辑。想要达到的使用效果如下:
public class TestTask extends SimpleJob {
@Override
public void execute(ShardingContext shardingContext) throws InterruptedException {
try {
while (true) {
log.info("TestTask start...");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
onExit(shardingContext);
}
}
@Override
private void onExit(ShardingContext shardingContext) {
System.out.println("TestTask exit");
}
}
改造
我们可以看到 JobScheduler#shutdown 方法最终底层的清理逻辑如下:
public void shutdown() {
setUpFacade.tearDown();
schedulerFacade.shutdownInstance();
jobExecutor.shutdown();
}
这个实现逻辑存在两个问题:
1,jobExecutor.shutdown 方法最终会调用 ExecutorService#shutdown 关闭执行线程池,这并不会中断执行任务的线程,一直等待任务自己执行完毕退出
2,没有调用 jobScheduleController.shutdown 方法,执行 Quartz 的退出逻辑
第一个问题比较容易理解:ThreadPool 的 shutdown 方法并不会中断处于 active 状态的线程,因此如果定时任务正在执行,shutdown 无法中断线程,应该改为调用 shutdownNow 方法。
至于第二个问题,我们先要了解 elastic-job 的定时任务执行逻辑。相关代码如下:
// org.apache.shardingsphere.elasticjob.kernel.executor.ElasticJobExecutor#process(org.apache.shardingsphere.elasticjob.api.JobConfiguration, org.apache.shardingsphere.elasticjob.spi.listener.param.ShardingContexts, org.apache.shardingsphere.elasticjob.spi.tracing.event.JobExecutionEvent.ExecutionSource)
private void process(final JobConfiguration jobConfig, final ShardingContexts shardingContexts, final ExecutionSource executionSource) {
Collection<Integer> items = shardingContexts.getShardingItemParameters().keySet();
// 当任务只有一个分片的时候,直接在 Quartz 的线程上执行任务
if (1 == items.size()) {
int item = shardingContexts.getShardingItemParameters().keySet().iterator().next();
JobExecutionEvent jobExecutionEvent = new JobExecutionEvent(IpUtils.getHostName(), IpUtils.getIp(), shardingContexts.getTaskId(), jobConfig.getJobName(), executionSource, item);
process(jobConfig, shardingContexts, item, jobExecutionEvent);
return;
}
CountDownLatch latch = new CountDownLatch(items.size());
// 当任务有多个分片的时候,在 elastic-job 自身的线程池上执行任务
for (int each : items) {
JobExecutionEvent jobExecutionEvent = new JobExecutionEvent(IpUtils.getHostName(), IpUtils.getIp(), shardingContexts.getTaskId(), jobConfig.getJobName(), executionSource, each);
ExecutorService executorService = executorServiceReloader.getExecutorService();
if (executorService.isShutdown()) {
return;
}
executorService.submit(() -> {
try {
process(jobConfig, shardingContexts, each, jobExecutionEvent);
} finally {
latch.countDown();
}
});
}
// ...
}
因此,仅中断 elastic-job 线程池中的线程是不够的,还需要中断 Quartz 线程池中的线程。
Quartz 的退出
通过查看 QuartzScheduler#shutdown(boolean) 方法,我们可以看到,当 Quartz 的 org.quartz.scheduler.interruptJobsOnShutdown 配置项配置为 true 的时候,且 Job 实现了 InterruptableJob 接口,则 Quartz 退出的时候会终端相关的 Job 对象
public void shutdown(boolean waitForJobsToComplete) {
if (!this.shuttingDown && !this.closed) {
// ...
// interruptJobsOnShutdown 配置为 true 时,且 waitForJobsToComplete 为 false时
// InterruptableJob 接口的相关实现会被中断
if (this.resources.isInterruptJobsOnShutdown() && !waitForJobsToComplete || this.resources.isInterruptJobsOnShutdownWithWait() && waitForJobsToComplete) {
for(JobExecutionContext job : this.getCurrentlyExecutingJobs()) {
if (job.getJobInstance() instanceof InterruptableJob) {
try {
((InterruptableJob)job.getJobInstance()).interrupt();
} catch (Throwable e) {
this.getLog().warn("Encountered error when interrupting job {} during shutdown: {}", job.getJobDetail().getKey(), e);
}
}
}
}
}
LiteJob 类是 Quartz 框架和 elastic-job 框架的适配器类,使得 Quartz 可以定时调用 elastic-job 中注册的任务。如果要让其实现 InterruptableJob 接口并支持线程中断,可以进行如下改造:
@Setter
public final class LiteJob implements InterruptableJob {
private ElasticJobExecutor jobExecutor;
private volatile Thread currentThread;
@Override
public void execute(final JobExecutionContext context) {
try {
// 在调用时,持有执行线程的引用
currentThread = Thread.currentThread();
jobExecutor.execute();
} finally {
currentThread = null;
}
}
@Override
public void interrupt() throws UnableToInterruptJobException {
// 在中断时,持有执行线程的引用进行中断
if (Objects.nonNull(currentThread)) {
currentThread.interrupt();
}
}
}
最终改动内容如下:
ExecutorServiceReloader#close方法改为调用executorService.shutdownNow方法JobScheduler#shutdown方法增加调用jobScheduleController.shutdown(false)逻辑JobScheduler#getQuartzProps增加org.quartz.scheduler.interruptJobsOnShutdown=true的配置LiteJob类实现InterruptableJob接口
具体改动请参考:PR #2475
使用
经过以上改造后,我们编写的定时任务可以通过捕获 InterruptedException 的方式感知到 elastic-job 的退出,并执行自定义的退出逻辑。比如通过注册 shutdownHook 的方式执行退出逻辑:
public class TestTask extends SimpleJob {
@Override
public void execute(ShardingContext shardingContext) {
try {
while (true) {
log.info("TestTask start...");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
onExit(shardingContext);
}
}
@Override
protected void onExit(ShardingContext shardingContext) {
// 执行自定义退出逻辑
System.out.println("TestTask exit");
}
}
public void registerShutdownHook() {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
// 获取 JobScheduler 的引用,调用 shutdown 方法退出 elastic-job
getJobScheduler().shutdown();
}));
}