Class ZipFsCloseRaceTestHook

java.lang.Object
net.fabricmc.loom.util.ZipFsCloseRaceTestHook

@Internal public final class ZipFsCloseRaceTestHook extends Object
仅供测试使用的注入钩子:让「共享 zipfs 实例被并发持有者关掉」这一竞态可以被确定性触发.

这不是功能开关,禁止在生产构建里设置它——属性名带 loom.test. 前缀即表示只在测试上下文有意义。 默认(不设属性)armForRead(List) 只读一次系统属性就返回,对被注入的代码路径零影响。

为什么需要它

TinyRemapperService.recoverClasspathRead 的受害侧自愈只在「读取 classpath 期间共享 zipfs 实例被并发持有者关掉」这一竞态下才会被走到,而这个竞态在真实构建里极难命中(上一轮连续全量构建 0 命中), 因此该分支此前只有单测能证明、端到端从未被走到。本钩子把该形态变成可注入、可断言的事件。

注入的形态与真实故障一致

刻意不复刻「登记簿里已有死条目」那条路径(单测已覆盖,它的成因是关闭时文件不在盘上)。这里复刻的是上一轮 端到端取证的形态:读取方(tiny-remapper 的 FileSystemReference.openJar)自己按 URI 路线打开 jar, 实例因此进入 JDK 进程级 zipfs 登记簿;本钩子检测到该条目出现——它的出现即「读取方已经拿到并在使用这个共享 实例」——再由一个并发的持有者线程把它关掉,读取方于是在遍历途中撞上 ClosedFileSystemException。 因此钩子只需一个注入点:armForRead(List),在读 classpath 之前布置好那个并发持有者。

关闭前为什么等一小会儿

遍历 jar 做的第一件事是对根目录调 ZipFileSystem.isDirectory,而该方法没有 ensureOpen 检查: 关闭若恰好落在「根目录取属性之后、列目录之前」那个微秒级窗口里,读取方拿到的是 NullPointerException 而不是 ClosedFileSystemException(实测扁平 victim jar 下约 2%,目录层数越多越容易命中), 自愈分支只认后者,失败类型就变成了随机。等 CLOSE_DELAY_MS 毫秒再关,遍历早已铺开、 根目录那次调用已经过去(victim jar 条目数由测试保证足够多,遍历窗口远大于该延迟),失败类型稳定在 CFSE 上。 这个延迟只是把关闭点挪进「读取确实在进行」的区间内,不改变形态本身。

需要命中 NPE 那一侧落点(目录遍历撞上已置空的 inodes)的用例可以用 DELAY_PROPERTY 把延迟调到 0:目录越多,列目录的次数越多,关闭点落在「属性已取到、正要列目录」 那个窗口里的机会也越多。缺省值不变,不设该属性时行为与本属性加入之前一致。

第二种注入:让读取以「形态被抹掉」的 NPE 失败

上面那条竞态命中的是 NPE 落点时,异常形态本身就带证据(消息指名 inodes、栈里有 zipfs 帧)。 但长命 JVM 里同一个隐式空指针被反复抛出后,HotSpot 会改用预分配的「fast throw」异常:消息为 null、 栈长为 0,判据拿不到任何形态证据。这条子形态由 READ_FAILURE_PROPERTY 注入:钩子让读取直接抛出 一个与 fast throw 逐字同形的 NPE(见 fastThrowNullPointer()),用来确定性验证 TinyRemapperService 的「重读治不好就原样抛出」这条行为判据——真实 fast throw 由 JIT 决定何时启用, 在单测里无法稳定触发。
  • Method Details

    • throwIfReadFailureArmed

      public static void throwIfReadFailureArmed()
      classpath 读取的唯一出口调用它:布置了读取失败("loom.test.zipfs.readFailure")就按形态抛出, 否则立即返回.

      不设属性时只读一次系统属性就返回,对被注入的代码路径零影响.

    • fastThrowNullPointer

      public static NullPointerException fastThrowNullPointer()
      与 HotSpot fast throw 逐字同形的 NPE 实例(见 FAST_THROW_NULL_POINTER).

      暴露出来只为让用例能断言「抛出的就是这同一个实例(对象标识相同,因而逐字一致)」;生产代码不使用.

    • armForRead

      public static void armForRead(List<Path> toRead)
      在本次 classpath 读取之前布置关闭竞态;不设 "loom.test.zipfs.closeRaceEntry" 时是空操作.

      只认第一个文件名包含标记的条目,因此夹具工程必须保证该标记只命中它自己那条受害者依赖。

      Parameters:
      toRead - 本次即将读取的 classpath 条目