Fix intermittent 'Saving world' freeze on shutdown

killAllLoops() only cancelled the next scheduled tick without waiting for
in-flight dimension ticks to finish. The main thread then ran saveAllChunks
concurrently with worker threads (level.tick -> processUnloads) mutating the
non-thread-safe ChunkMap.toDrop, corrupting the fastutil map and hanging the
save forever.

Add ServerManager.stopLoopsAndWaitIdle() that kills all loops and spins until
each loop isTicking()==false (30s watchdog), and call it first in
MinecraftServerDimMixin.stopServer before killAllLoops(), so dimension ticks
fully finish before the main thread starts saving.
This commit is contained in:
xxdd001 committed 2026-09-07 19:17:54 +08:00
1 parent da3246878b
commit 3dc6f4141f
2 files changed
+36 -1

No files matched your search

@@ -11,6 +11,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.LockSupport;
/** /**
* 每服务器激活状态与线程池管理(dimthreads 模型)。 * 每服务器激活状态与线程池管理(dimthreads 模型)。
@@ -201,6 +202,35 @@ public class ServerManager {
} }
} }
/**
* 停止所有维度 loop 并<b>等待正在执行的 tick 完全收口</b>(服务器停机时调用)。
* <p>
* 仅 {@link #killAllLoops()}(置 running=false + cancel 下一次自排程)不等待正在运行的 tick:
* {@code cancel(true)} 只能取消尚未开始的任务,对正在执行 {@code LevelTickLoop.internalTick}
* 的维度 worker 无效。于是 stopServer 之后主线程的 {@code saveAllChunks} 会与仍在飞的维度
* worker({@code level.tick → processUnloads → ChunkMap.toDrop})并发修改非线程安全的
* {@code ChunkMap.toDrop}(fastutil Long2ObjectLinkedOpenHashMap),结构损坏后主线程保存
* 无限卡死(表现为「正在保存中」冻结,只能强杀后台进程)。
* <p>
* 因此先 kill 全部 loop(停掉下一拍,配合 ticking 标志阻止再进入),再自旋等待每个 loop 的
* {@code isTicking()==false}(worker 已完全退出当前 tick,含内部子任务屏障收口),
* 带 30s 看门狗防止异常场景下永久阻塞。
*/
public void stopLoopsAndWaitIdle() {
this.loops.values().forEach(LevelTickLoop::kill);
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(30);
for (LevelTickLoop loop : this.loops.values()) {
while (loop.isTicking()) {
if (System.nanoTime() > deadline) {
ModZuozhi.LOGGER.warn("[DimThread] 等待维度 {} loop 收口超时(>30s),强制继续",
loop.getLevel().dimension().location());
break;
}
LockSupport.parkNanos(100_000L); // 0.1ms 退避
}
}
}
/** 停止所有维度 loop 并关闭调度池(服务器停机 / 全局开关关闭时调用)。 */ /** 停止所有维度 loop 并关闭调度池(服务器停机 / 全局开关关闭时调用)。 */
public void killAllLoops() { public void killAllLoops() {
this.loops.values().forEach(LevelTickLoop::kill); this.loops.values().forEach(LevelTickLoop::kill);
@@ -78,9 +78,14 @@ public abstract class MinecraftServerDimMixin {
ci.cancel(); ci.cancel();
} }
/** 服务器停止时:先停维度 loop,再关闭线程池,防止挂起。 */ /** 服务器停止时:先等所有维度 tick 收口,再停 loop、关线程池,防止保存与 worker 并发卡死。 */
@Inject(method = "stopServer", at = @At("HEAD")) @Inject(method = "stopServer", at = @At("HEAD"))
public void modzuozhi_shutdownThreadpool(CallbackInfo ci) { public void modzuozhi_shutdownThreadpool(CallbackInfo ci) {
// 必须先等正在执行的维度 tick 完全退出再进入保存:否则主线程 saveAllChunks 会与
// 仍在飞的维度 worker(level.tick → processUnloads)并发修改 ChunkMap.toDrop
// (非线程安全 fastutil 结构),导致「正在保存中」无限卡死(只能强杀进程)。
// stopLoopsAndWaitIdle 先 kill 下一拍、再自旋等 isTicking()==false(30s 看门狗)。
DimThreadCore.MANAGER.stopLoopsAndWaitIdle();
DimThreadCore.MANAGER.killAllLoops(); DimThreadCore.MANAGER.killAllLoops();
DimThreadCore.MANAGER.threadPools.forEach((server, pool) -> pool.shutdown()); DimThreadCore.MANAGER.threadPools.forEach((server, pool) -> pool.shutdown());
DimThreadCore.MANAGER.clear(); DimThreadCore.MANAGER.clear();