我有一个 spring boot / spring batch 应用程序,它启动不同的作业。
当应用程序停止 (
CTRL-C
) 时,作业将保持运行状态 (STARTED)。CTRL-C
为应用程序提供了足够的时间来优雅地停止作业,但结果与 kill -9
相同。
我找到了一种方法(见下文),可以在使用
CTRL-C
终止应用程序时优雅地停止所有作业,但我 想知道是否有更好/更简单的方法来实现此目标。
下面的所有内容都是关于我如何设法停止工作的文档。
"在 부알프레도 的 博客条目中,
JobExecutionListener
用于注册应该停止作业的关闭钩子:\n"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 Boot 在停止作业之前不会关闭 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
。
如果您可以稍等一下直到某个步骤完成,假设一个步骤持续 20 秒,并且您已将超时设置为 30 秒,则可以执行以下操作。这可以防止作业出现不需要的
ExitStatus
并将其设置为 Stopped (to restart)
。您可以确保在编写器完成之前执行完整的步骤,而不仅仅是读取器或处理器,也不仅仅是编写器执行的一部分。
@Component
class GracefulShutdownStepExecutionListener : StepExecutionListener {
@EventListener(ContextClosedEvent::class)
fun onContextClosed() {
isContextClosing = true
}
override fun beforeStep(p0: StepExecution) {
// do nothing
}
override fun afterStep(p0: StepExecution): ExitStatus? {
if (!isContextClosing) {
return ExitStatus.EXECUTING
}
return ExitStatus.STOPPED
}
companion object {
@Volatile
private var isContextClosing = false;
}
}