elastic-job 增加退出通知

56 阅读3分钟

背景

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();  
        }  
    }  
}

最终改动内容如下:

  1. ExecutorServiceReloader#close 方法改为调用 executorService.shutdownNow 方法
  2. JobScheduler#shutdown 方法增加调用 jobScheduleController.shutdown(false) 逻辑
  3. JobScheduler#getQuartzProps 增加 org.quartz.scheduler.interruptJobsOnShutdown=true 的配置
  4. 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();
	}));
}