在 Worker 执行重任务时,`CommonTaskTracker` 的定时线程正在访问 `DbTaskPersistenceService` / H2 数据库。如果此时触发 `HeavyTaskTracker.destroy()`,其中会对 `scheduledPool` 执行 `shutdownNow()`,可能中断正在执行 H2 SQL 的线程,导致 H2 数据库被关闭或文件异常。
Author: imzhonghuaCreated Jun 12, 2026Updated Jun 12, 2026
Labelsbug
问题描述
在 Worker 执行重任务时,CommonTaskTracker 的定时线程正在访问 DbTaskPersistenceService / H2 数据库。如果此时触发 HeavyTaskTracker.destroy(),其中会对 scheduledPool 执行 shutdownNow(),可能中断正在执行 H2 SQL 的线程。
由于 H2 底层使用 FileChannel / MVStore,线程中断可能导致底层 FileChannel 被关闭,进而出现:
org.h2.jdbc.JdbcSQLNonTransientConnectionException: The database has been closed [90098-214]
Caused by: org.h2.mvstore.MVStoreException: Reading from file ... failed
Caused by: java.nio.channels.ClosedChannelException
想确认:这里是否存在因 shutdownNow() 中断正在执行数据库 I/O 的线程,导致 H2 数据库被关闭甚至文件异常的风险?
相关代码位置
HeavyTaskTracker.destroy()
// tech.powerjob.worker.core.tracker.task.heavy.HeavyTaskTracker
@Override
public void destroy() {
finished.set(true);
// 0. 开始关闭线程池,不能使用 shutdownNow(),因为 destroy 方法本身就在 scheduledPool 的线程中执行,强行关闭会打断 destroy 的执行。
scheduledPool.shutdown();
// ...
// 2. 删除所有数据库数据
boolean dbSuccess = taskPersistenceService.deleteAllTasks(instanceId);
// ...
// 4. 强制关闭线程池
if (!scheduledPool.isTerminated()) {
CommonUtils.executeIgnoreException(() -> scheduledPool.shutdownNow());
}
}
CommonTaskTracker 状态检查线程
// tech.powerjob.worker.core.tracker.task.heavy.CommonTaskTracker
scheduledPool.scheduleWithFixedDelay(new StatusCheckRunnable(), 3, delay, TimeUnit.SECONDS);
StatusCheckRunnable 中会查询任务状态:
InstanceStatisticsHolder holder = getInstanceStatisticsHolder(instanceId);
最终调用:
taskPersistenceService.getTaskStatusStatistics(instanceId, subInstanceId);
DbTaskPersistenceService
private <T> T execute(SupplierPlus<T> executor, Consumer<Long> slowQueryLogger) throws Exception {
long s = System.currentTimeMillis();
try {
return CommonUtils.executeWithRetry(executor, RETRY_TIMES, RETRY_INTERVAL_MS);
} finally {
long cost = System.currentTimeMillis() - s;
if (cost > SLOW_QUERY_RT_THRESHOLD) {
slowQueryLogger.accept(cost);
}
}
}
TaskDAOImpl.simpleQueryPlus
try (Connection conn = connectionFactory.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
rs = ps.executeQuery();
...
}
异常堆栈
[06-12 05:30:58.812] ERROR [] powerjob-worker-batch ctttp-945530341840388544-0 tech.powerjob.worker.persistence.DbTaskPersistenceService [] [TaskPersistenceService] getTaskStatusStatistics for instance(id=945530341840388544) failed.
org.h2.jdbc.JdbcSQLNonTransientConnectionException: The database has been closed [90098-214]
at org.h2.message.DbException.getJdbcSQLException(DbException.java:678)
at org.h2.message.DbException.getJdbcSQLException(DbException.java:477)
at org.h2.message.DbException.get(DbException.java:212)
at org.h2.engine.SessionLocal.getTransaction(SessionLocal.java:1596)
at org.h2.engine.SessionLocal.startStatementWithinTransaction(SessionLocal.java:1617)
at org.h2.command.Command.executeQuery(Command.java:184)
at org.h2.jdbc.JdbcPreparedStatement.executeQuery(JdbcPreparedStatement.java:128)
at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeQuery(ProxyPreparedStatement.java:52)
at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeQuery(HikariProxyPreparedStatement.java)
at tech.powerjob.worker.persistence.db.TaskDAOImpl.simpleQueryPlus(TaskDAOImpl.java:129)
at tech.powerjob.worker.persistence.DbTaskPersistenceService.lambda$getTaskStatusStatistics$16(DbTaskPersistenceService.java:287)
at tech.powerjob.common.utils.CommonUtils.executeWithRetry(CommonUtils.java:47)
at tech.powerjob.worker.persistence.DbTaskPersistenceService.execute(DbTaskPersistenceService.java:399)
at tech.powerjob.worker.persistence.DbTaskPersistenceService.getTaskStatusStatistics(DbTaskPersistenceService.java:286)
at tech.powerjob.worker.persistence.SwapTaskPersistenceService.getTaskStatusStatistics(SwapTaskPersistenceService.java:174)
at tech.powerjob.worker.core.tracker.task.heavy.HeavyTaskTracker.getInstanceStatisticsHolder(HeavyTaskTracker.java:440)
at tech.powerjob.worker.core.tracker.task.heavy.CommonTaskTracker$StatusCheckRunnable.innerRun(CommonTaskTracker.java:165)
at tech.powerjob.worker.core.tracker.task.heavy.CommonTaskTracker$StatusCheckRunnable.run(CommonTaskTracker.java:322)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.runAndReset(FutureTask.java:308)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$301(ScheduledThreadPoolExecutor.java:180)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:294)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:855)
Caused by: org.h2.mvstore.MVStoreException: Reading from file sun.nio.ch.FileChannelImpl@e0a6f13 failed at 7069598499 (length -1), read 0, remaining 8192 [2.1.214/1]
at org.h2.mvstore.DataUtils.newMVStoreException(DataUtils.java:1004)
at org.h2.mvstore.DataUtils.readFully(DataUtils.java:470)
at org.h2.mvstore.FileStore.readFully(FileStore.java:98)
at org.h2.mvstore.Chunk.readBufferForPage(Chunk.java:422)
at org.h2.mvstore.MVStore.readPage(MVStore.java:2569)
at org.h2.mvstore.MVMap.readPage(MVMap.java:633)
at org.h2.mvstore.Page$NonLeaf.getChildPage(Page.java:1125)
at org.h2.mvstore.CursorPos.traverseDown(CursorPos.java:61)
at org.h2.mvstore.MVMap.operate(MVMap.java:1770)
at org.h2.mvstore.MVMap.put(MVMap.java:156)
at org.h2.mvstore.MVStore.acceptChunkOccupancyChanges(MVStore.java:1894)
at org.h2.mvstore.MVStore.serializeToBuffer(MVStore.java:1675)
at org.h2.mvstore.MVStore.serializeAndStore(MVStore.java:1598)
at org.h2.mvstore.MVStore.lambda$storeNow$4(MVStore.java:1518)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
... 3 common frames omitted
Caused by: java.nio.channels.ClosedChannelException
at sun.nio.ch.FileChannelImpl.ensureOpen(FileChannelImpl.java:110)
at sun.nio.ch.FileChannelImpl.read(FileChannelImpl.java:715)
at org.h2.mvstore.DataUtils.readFully(DataUtils.java:456)
... 17 common frames omitted
初步分析
从堆栈看,异常发生在 ctttp-{instanceId}-0 线程中,该线程属于 CommonTaskTracker 的 scheduledPool。
该线程正在执行:
StatusCheckRunnable
→ getInstanceStatisticsHolder
→ getTaskStatusStatistics
→ TaskDAOImpl.simpleQueryPlus
→ PreparedStatement.executeQuery
→ H2 MVStore FileChannel read
如果此时 HeavyTaskTracker.destroy() 执行到:
scheduledPool.shutdownNow();
则会对 scheduledPool 中正在运行的线程执行 interrupt。
Java NIO 的 FileChannel 属于可中断通道,线程在执行文件 I/O 时被 interrupt,可能导致 channel 被关闭,从而触发:
java.nio.channels.ClosedChannelException
随后 H2 MVStore 抛出:
MVStoreException: Reading from file ... failed
最终表现为:
JdbcSQLNonTransientConnectionException: The database has been closed
疑问
1. HeavyTaskTracker.destroy() 中最后的 scheduledPool.shutdownNow() 是否可能中断正在执行 H2 SQL 的状态检查线程?
2. H2 MVStore 在被 interrupt 后出现 ClosedChannelException,是否可能导致整个 Worker 内置数据库被标记为 closed?
3. 这里是否应该避免在 destroy() 中使用 shutdownNow(),改为更温和的关闭方式,例如:
- 仅使用 scheduledPool.shutdown();
- 或先 shutdown(),再 awaitTermination();
- 超时后再根据场景决定是否 shutdownNow();
- 或确保数据库访问完全结束后再中断线程池。
4. CommonUtils.executeWithRetry() 中目前会吞掉第一次 executor.get() 的异常,如果随后 Thread.sleep() 被 interrupt,日志中可能只看到 sleep interrupted,是否也需要改进异常保留逻辑?
期望行为
当任务结束或 TaskTracker.destroy() 执行时,不应因为强制中断正在执行数据库 I/O 的线程,导致 H2 数据库被关闭或文件异常。
实际行为
StatusCheckRunnable 正在查询 H2 时,疑似被 shutdownNow() 中断,出现:
The database has been closed [90098-214]
Caused by: MVStoreException
Caused by: ClosedChannelException
环境信息
- PowerJob 版本:5.1.1
- H2 版本:2.1.214
- JDK 版本:8
- Worker 存储策略:DISK
- 任务类型:MAP_REDUCE
- 是否使用 SWAP:是
可能的修复方向
建议考虑:
1. HeavyTaskTracker.destroy() 不再直接 shutdownNow();
2. 对 scheduledPool 使用优雅关闭流程;
3. DbTaskPersistenceService / CommonUtils.executeWithRetry() 对 InterruptedException 做特殊处理,恢复中断标记并保留原始 SQL 异常;
4. 在 Worker 销毁时,保证数据库相关线程池先优雅停止,再释放底层 H2/Hikari 资源Source: PowerJob/PowerJob