在关闭时停止所有 spring 批处理作业 (CTRL-C)

Stop all spring batch jobs at shutdown (CTRL-C)

我有一个 spring boot / spring batch 应用程序,它启动了不同的作业。

应用程序停止时 (CTRL-C),作业仍处于 运行 状态 (STARTED)。
即使 CTRL-C 给了应用程序足够的时间来优雅地停止作业,结果与 kill -9.

相同

我找到了一种方法(见下文)在使用 CTRL-C 终止应用程序时优雅地停止所有作业,但 想知道是否有更好/更简单的方法实现这个目标。

下面的所有内容都是关于我如何设法停止作业的文档。

blog entry from 부알프레도 中,JobExecutionListener 用于注册应该停止作业的关闭挂钩:

public class ProcessShutdownListener implements JobExecutionListener {
    private final JobOperator jobOperator;
    ProcessShutdownListener(JobOperator jobOperator) { this.jobOperator = jobOperator; }
     
    @Override public void afterJob(JobExecution jobExecution) { /* do nothing. */ }
 
    @Override
    public void beforeJob(final JobExecution jobExecution) {
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                super.run();
                try {
                    jobOperator.stop(jobExecution.getId());
                    while(jobExecution.isRunning()) {
                        try { Thread.sleep(100); } catch (InterruptedException e) {}
                    }
                } catch (NoSuchJobExecutionException | JobExecutionNotRunningException e) { /* ignore */ }
            }
        });
    }
}

除了提供的代码外,我还必须创建一个 JobRegistryBeanPostProcessor
没有这个后处理器,jobOperator 将无法找到工作。
(NoSuchJobException: No job configuration with the name [job1] was registered

    @Bean
    public JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor(JobRegistry jobRegistry) {
        JobRegistryBeanPostProcessor postProcessor = new JobRegistryBeanPostProcessor();
        postProcessor.setJobRegistry(jobRegistry);
        return postProcessor;
    }

关闭挂钩无法将状态写入数据库,因为数据库连接已经关闭: org.h2.jdbc.JdbcSQLNonTransientConnectionException: Database is already closed (to disable automatic closing at VM shutdown, add ";DB_CLOSE_ON_EXIT=FALSE" to the db URL)

Processing item 2 before
Shutdown Hook is running !
2021-02-08 22:39:48.950  INFO 12676 --- [extShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2021-02-08 22:39:49.218  INFO 12676 --- [extShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.
Processing item 3 before
Exception in thread "Thread-3" org.springframework.transaction.CannotCreateTransactionException: Could not open JDBC Connection for transaction; nested exception is java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30004ms.

为了确保 spring 启动在停止作业之前不会关闭 hikari 数据源池,我使用了 SmartLifeCycle,如前所述 here

最后的 ProcessShutdownListener 看起来像:

@Component
public class ProcessShutdownListener implements JobExecutionListener, SmartLifecycle {
    private final JobOperator jobOperator;
    public ProcessShutdownListener(JobOperator jobOperator) { this.jobOperator = jobOperator; }

    @Override
    public void afterJob(JobExecution jobExecution) { /* do nothing. */ }

    private static final List<Runnable> runnables = new ArrayList<>();

    @Override
    public void beforeJob(final JobExecution jobExecution) {
        runnables.add(() -> {
                try {
                    if (!jobOperator.stop(jobExecution.getId())) return;
                    while (jobExecution.isRunning()) {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException ignored) { /* ignore */ }
                    }
                } catch (NoSuchJobExecutionException | JobExecutionNotRunningException e) { /* ignore */ }
            });
    }

    @Override
    public void start() {}

    @Override
    public void stop() {
//        runnables.stream()
//                .parallel()
//                .forEach(Runnable::run);
        runnables.forEach(Runnable::run);
    }

    @Override
    public boolean isRunning() { return true; }

    @Override
    public boolean isAutoStartup() { return true; }

    @Override
    public void stop(Runnable callback) { stop(); callback.run(); }

    @Override
    public int getPhase() { return Integer.MAX_VALUE; }
}

配置作业时必须注册此侦听器:

    @Bean
    public Job job(JobBuilderFactory jobs,
                   ProcessShutdownListener processShutdownListener) {
        return jobs.get("job1")
                .listener(processShutdownListener)
                .start(step(null))
                .build();
    }

最后如异常输出中提到的标志:;DB_CLOSE_ON_EXIT=FALSE 必须添加到 jdbc url.

这种方法是可行的方法,因为关闭挂钩是 JVM 提供的拦截外部信号的唯一方法(据我所知)。但是,这种方法不能保证有效,因为关闭挂钩不能保证被 JVM 调用。以下是 Runtime.addShutdownHook 方法的 Javadoc 的摘录:

In rare circumstances the virtual machine may abort, that is, stop running
without shutting down cleanly. This occurs when the virtual machine is 
terminated externally, for example with the SIGKILL signal on Unix or 
the TerminateProcess call on Microsoft Windows.

此外,关闭挂钩预计运行“快速”:

Shutdown hooks should also finish their work quickly. When a program invokes
exit the expectation is that the virtual machine will promptly shut down
and exit.

在你的例子中,JobOperator.stop涉及一个数据库事务(可能跨网络)来更新作业的状态,我不确定这个操作是否足够“快”。

附带说明一下,示例模块中有一个名为 GracefulShutdownFunctionalTests 的示例。此示例基于已弃用的 JobExecution.stop,但将更新为使用 JobOperator.stop.