Package net.fabricmc.loom.util
Class ZipFsCloseRaceTestHook
java.lang.Object
net.fabricmc.loom.util.ZipFsCloseRaceTestHook
仅供测试使用的注入钩子:让「共享 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 Summary
Modifier and TypeMethodDescriptionstatic voidarmForRead(List<Path> toRead) 在本次 classpath 读取之前布置关闭竞态;不设 "loom.test.zipfs.closeRaceEntry" 时是空操作.static NullPointerException与 HotSpot fast throw 逐字同形的 NPE 实例(见FAST_THROW_NULL_POINTER).static voidclasspath 读取的唯一出口调用它:布置了读取失败("loom.test.zipfs.readFailure")就按形态抛出, 否则立即返回.
-
Method Details
-
throwIfReadFailureArmed
public static void throwIfReadFailureArmed()classpath 读取的唯一出口调用它:布置了读取失败("loom.test.zipfs.readFailure")就按形态抛出, 否则立即返回.不设属性时只读一次系统属性就返回,对被注入的代码路径零影响.
-
fastThrowNullPointer
与 HotSpot fast throw 逐字同形的 NPE 实例(见FAST_THROW_NULL_POINTER).暴露出来只为让用例能断言「抛出的就是这同一个实例(对象标识相同,因而逐字一致)」;生产代码不使用.
-
armForRead
在本次 classpath 读取之前布置关闭竞态;不设 "loom.test.zipfs.closeRaceEntry" 时是空操作.只认第一个文件名包含标记的条目,因此夹具工程必须保证该标记只命中它自己那条受害者依赖。
- Parameters:
toRead- 本次即将读取的 classpath 条目
-