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:
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.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
|
||||
/**
|
||||
* 每服务器激活状态与线程池管理(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 并关闭调度池(服务器停机 / 全局开关关闭时调用)。 */
|
||||
public void killAllLoops() {
|
||||
this.loops.values().forEach(LevelTickLoop::kill);
|
||||
|
||||
@@ -78,9 +78,14 @@ public abstract class MinecraftServerDimMixin {
|
||||
ci.cancel();
|
||||
}
|
||||
|
||||
/** 服务器停止时:先停维度 loop,再关闭线程池,防止挂起。 */
|
||||
/** 服务器停止时:先等所有维度 tick 收口,再停 loop、关线程池,防止保存与 worker 并发卡死。 */
|
||||
@Inject(method = "stopServer", at = @At("HEAD"))
|
||||
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.threadPools.forEach((server, pool) -> pool.shutdown());
|
||||
DimThreadCore.MANAGER.clear();
|
||||
|
||||
Reference in new issue
Block a user