16 加载失败与降级机制
1 引言
前面的章节主要讨论正常路径:加载票把区块拉进系统,ChunkHolder 推进 Future,TACS 把 ProtoChunk 转成 WorldChunk,实体管理器挂载实体,世界层计划器再执行到期计划刻。
这是服务端希望看到的稳定态。理想情况下,区块从磁盘加载或从生成管线产出后,会顺着 ChunkStatus 进入 FULL,再根据加载级别获得 BLOCK_TICKING 或 ENTITY_TICKING 资格。玩家可以读写方块,实体正常运行,计划刻按世界时间触发。
但现实中的加载路径并不总是干净的。磁盘可能返回 IOException,region 文件可能损坏,生成器可能崩溃,数据修复器也可能抛错。
这些异常不会被统一处理。TACS 会区分 “局部 I/O 失败” 和 “逻辑崩溃”:前者通常降级,后者通常崩溃。
本章讨论加载失败后的降级机制:ThreadedAnvilChunkStorage 如何处理 IOException、生成异常和 crash report;为什么它会用空 ProtoChunk 继续流程;以及 EmptyChunk 和降级 ProtoChunk 到底有什么区别。
理解这些路径有两个价值:它能解释 region 文件损坏后为什么服务器没有立刻崩溃,也能帮助判断修复方向:IOException 指向 region 文件或磁盘层,生成器崩溃要看 crash report,降级 ProtoChunk 则意味着原区块数据没有被恢复。
本章仍以 Minecraft Java Edition 1.20.1、Yarn 1.20.1+build.10 为基准。源码引用沿用本系列格式,文件名和行号标在代码块注释或正文括号中。
2 加载失败的降级路径
2.1 recoverFromException():加载失败的兜底
TACS 的区块获取过程是一个 CompletableFuture 链。只要链路中某个阶段抛出异常,末尾的兜底函数就要决定:继续运行、返回替代区块,还是让服务器崩溃。异常入口在 ThreadedAnvilChunkStorage.getChunk() 的 Future 链末尾:exceptionallyAsync(throwable -> this.recoverFromException(throwable, pos), this.mainThreadExecutor)(ThreadedAnvilChunkStorage.java:613)。核心逻辑如下(ThreadedAnvilChunkStorage.java:620-633):
这里有三条路径。A. CrashException 且 cause 不是 IOException:这通常表示生成器、结构放置、数据转换或 Future 状态机内部出现了不可继续的逻辑错误。TACS 先调用 markAsProtoChunk(chunkPos)(ThreadedAnvilChunkStorage.java:624),再重新抛出 CrashException(ThreadedAnvilChunkStorage.java:625)。服务器会停机,并生成完整 crash report。
B. 纯 IOException 或 CrashException(IOException):这类错误更像存储层问题,例如磁盘读取失败、region 文件损坏、压缩数据不完整。TACS 只记录错误日志(ThreadedAnvilChunkStorage.java:628、630),然后继续向下返回替代区块。
C. 其他异常:代码没有额外日志分支,最终同样返回空 ProtoChunk。真正应该崩溃的内部错误通常会被包装成 CrashException,并走第一条路径。
最后 Future 链返回 Either.left(this.getProtoChunk(chunkPos)):它没有返回 Unloaded,而是返回一个新建的空 ProtoChunk。
2.2 getProtoChunk():降级区块的生成
降级区块来自 getProtoChunk()(ThreadedAnvilChunkStorage.java:636-639):
这个 ProtoChunk 是最小替代物,不携带原区块的方块、实体、方块实体、光照或结构信息。getProtoChunk() 的第一行会调用 markAsProtoChunk()(ThreadedAnvilChunkStorage.java:641-643):
chunkToType 是 TACS 用来记录区块类型的 Long2ByteOpenHashMap。这里写入 (byte)-1,表示这个位置当前被视为 ProtoChunk,后续调用 isLevelChunk(pos) 时不会把它误判成正常 WorldChunk。降级 ProtoChunk 的特征可以概括为:
2.3 crash():崩溃报告的生成
不是所有异常都会降级。TACS 内部还有一个专门的 crash() 方法,用来把 “区块加载流程出现不一致” 转换成 CrashException。关键逻辑如下(ThreadedAnvilChunkStorage.java:364-390):
它会遍历 currentChunkHolders 和 chunkHolders 两套视图,对每个 ChunkHolder 调用 collectFuturesByStatus(),再收集一种特别危险的状态:Future 非空、已经完成,但 join() 得到 null。
正常区块 Future 应该完成为 Either.left(Chunk) 或 Either.right(Unloaded)。完成为 null 意味着 Future 链破坏了自己的类型约定。crash report 会记录触发崩溃的 details、异常 Future 所属坐标、对应 ChunkStatus,以及 Updating / Visible 两套视图中的异常项。
2.4 EmptyChunk:临时虚空区块
除了降级 ProtoChunk,源码里还有一个名字更直观的空区块:EmptyChunk。它继承自 WorldChunk,但覆盖了关键行为(EmptyChunk.java:16-82):
EmptyChunk 主要用于临时视图。比如世界生成时的 ChunkCache 找不到目标区块,就会返回它(ChunkCache.java:73-75):
EmptyChunk 和降级 ProtoChunk 的区别如下:
3 与现象章节的关系
本章只保留加载失败后的降级机制:recoverFromException()、getProtoChunk()、markAsProtoChunk()、EmptyChunk 与 crash report。保存失败、区块互换和 PENDING 卡住已经独立为现象章节:
4 小结
区块加载失败时,ThreadedAnvilChunkStorage.recoverFromException() 会按异常类型选择崩溃或降级。非 I/O 的 CrashException 会继续抛出并生成 crash report;IOException 或 CrashException(IOException) 会被记录日志,并降级为空 ProtoChunk。
降级 ProtoChunk 不是恢复出的原区块;它没有原始方块、实体、方块实体、光照或结构数据。markAsProtoChunk() 会把 chunkToType 标成 -1,避免后续把降级对象误判成正常 WorldChunk。EmptyChunk 则是 ChunkCache 等临时视图里的虚空区块,不等同于加载失败后的替代结果。
5 代码走读
5.1 recoverFromException() 的异常分类
再看 ThreadedAnvilChunkStorage.java:620-633:
这里最容易漏掉的是 CrashException(IOException)。TACS 会拆出 cause:如果 cause 是 IOException,它仍按 I/O 失败处理,只记录日志并返回空 ProtoChunk;如果 cause 不是 IOException,它只先调用 markAsProtoChunk() 留下类型标记,然后 throw crashException,确保 crash report 不会被替代区块掩盖。
5.2 getProtoChunk() 与 markAsProtoChunk()
getProtoChunk() 本身很短(ThreadedAnvilChunkStorage.java:636-639):
登记逻辑在 ThreadedAnvilChunkStorage.java:641-643:
chunkToType 的值不是区块内容,而是 TACS 对该位置最近保存 / 加载类型的旁路记忆。降级路径先写 -1,是在告诉后续逻辑:“这个对象只是 proto 级别的替代结果,不要把它当成已经完整加载的 level chunk。”
5.3 crash() 的 Future 收集逻辑
crash() 的过滤条件是:
它没有收集所有未完成 Future,也没有收集所有异常 Future。未完成 Future 在异步系统里是正常状态;异常 Future 会进入恢复或崩溃路径。真正违反约定的是 “已经完成,但完成值是 null”。区块 Future 的结果类型是 Either<Chunk, ChunkHolder.Unloaded>,所以 crash() 把这些 holder 和 status 写进 report。
6 参考
net.minecraft.server.world.ThreadedAnvilChunkStoragenet.minecraft.world.chunk.EmptyChunknet.minecraft.util.crash.CrashReport
