Skip to content

[fix] 用原子缓存修复 KubeJS Lazy 竞态并避免回调持锁 - #131

Draft
wanquanw wants to merge 2 commits into
1.20.1from
codex/fix-kubejs-lazy-synchronization
Draft

wanquanw wants to merge 2 commits into
1.20.1from
codex/fix-kubejs-lazy-synchronization

Conversation

@wanquanw

@wanquanw wanquanw commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

问题与最终实现

KubeJS 自身的资源读取通过 GeneratedData.get() 调用 Lazy.get(),并可能随后 forget();并发读写分离的 cached/value 字段时,正常 supplier 也可能返回空缓存。这条路径不依赖 ProbeJS。关联 CDR #1736CDR #2309

本 PR 通过 CDC Mixin 插件为每个 Lazy 添加独立的原子缓存状态,修复该竞态,同时避免持有 Lazy 锁执行任意 factory 回调:

  • LazyMixin 使用字符串目标、@Pseudo、关闭 remap,并按 KubeJS 是否存在门控。
  • 插件校验构造器、字段与 get/forget 描述符后,初始化独立的 KubeJsLazyCache,将两个方法桥接到 helper;不引入 KubeJS 编译依赖。
  • AtomicReference<Snapshot> 一次读取/发布 cached 与 value,合法 null 也能缓存;factory 执行期间不持缓存锁、不等待其他计算。
  • factory 返回后只尝试一次 CAS,当前调用返回自己的局部结果。forget() 发布新的空快照,使失效前开始的计算无法回填或覆盖失效后的新值。
  • 保留绝对过期时间和异常传播。并发 miss 仍可重复执行 factory(原版也可能如此),不承诺“恰好一次”;同一快照下先 CAS 成功者填充缓存。

本实现替换 get/forget 方法体并初始化新字段,已不属于只修改访问标志的等价补丁;升级 KubeJS 或与其他同目标补丁共存时必须复核。父整合包 HotAI、Packwiz、子模块指针、运行 JAR 和 CDC 版本号均未修改。

死锁审计依据

  • KubeJS ResourceGenerator.add 接受任意 Supplier<byte[]>Utils.lazy/expiringLazy 暴露任意脚本 supplier,并有后台线程 API;BlockEntityAttachmentType.ALL 的 factory 还会执行插件注册回调。因此不能为所有 factory 保证统一的外部锁顺序。
  • 对真实 Lazy class 构造 externalLock -> Lazy.get()Lazy.get() -> factory -> externalLock 的受控夹具:原版和原子缓存版均完成,整方法 synchronized 版被 JVM ThreadMXBean 确认存在两个线程的监视器死锁。
  • 扫描本地 388 个运行 JAR,排除 KubeJS 自身之后,只有 ProbeJS 6.0.1 直接引用该类,其 dump 在异步编译线程调用;没有找到其外部调用自身的额外锁/等待,也没有找到整合包脚本直接使用 Lazy 异步 API。这只是外部直接引用清单,不表示只有 ProbeJS 使用 Lazy,不否定 KubeJS 自身的资源读取竞态。
  • 尚未定位真实游戏中的锁环,也未定位 #1736 原报告的具体资源/竞争线程。受控死锁是同步方案新增风险的证据,不是原存档死锁复现。

已完成验证

Java 17.0.12 / Gradle 8.8 完整构建、reobf 与 git diff --check 通过。测试直接调用生产插件改写真实发行 JAR 中的 Lazy.class,并加载 CDC helper 执行:

./gradlew build kubeJsLazyRegression kubeJsLazyLockOrderProbe -PkubejsLazyJar=/path/to/kubejs.jar --no-daemon
KubeJS 8 线程共 200 万轮 get/forget 锁顺序探针
2001.6.5-build.16 原版空值 38,888,新版 0 原版/原子版完成,同步版出现预期对照死锁
2001.6.5-build.24 新版空值 0 同上
2001.6.5-build.26 原版空值 98,332,新版 0 同上

build.16 测试 JAR 的 SHA-256 为 3de6b7267d3aab981848ed54d3afe7edf20532fa4060631fd6fffcd99ac5f3d5,与 #2309 的输入一致。原版空值次数随线程调度变化。

同时通过:改写范围校验、幂等、结构不符时不部分修改、缓存命中、合法 null、过期、supplier 异常后跨线程重试、计算中连续 forget(含新值已填充/未填充两种情况)、并发 miss,以及 factory 等待另一线程执行 forget。

独立测试没有启动完整 Forge/Mixin 加载管线,不代表模组门控、辅助类加载时序、冷启动进档或游戏 /reload 已经验证。原子缓存自身不建立回调锁依赖,但也不保证任意脚本/模组本身不会死锁,不承诺零性能损失。

待测与迁移

  • 在存档副本部署测试 CDC,移走 hotai/dev/latvian/mods/kubejs/util/Lazy.badiff 后完整重启;不要叠加旧二进制差分。
  • 确认 [CDCore][KubeJSLazy] Applied atomic cache 日志;导出 class 含 cache 字段/构造器初始化/helper 调用,get/forget 无 ACC_SYNCHRONIZED
  • 冷启动首次进档、退出重进、连续重载、保存后再次冷启动;检查饰品及背包内容。
  • 检查 GeneratedData 空值、TACZ GUN_DATA=nullCouldn't place player;验证 TACZ 同步、JEI 注液配方/流体标签。
  • 重载停顿时采集至少两份间隔数秒的线程转储,检查锁持有者及等待环;记录重载耗时。
  • 独立服务器登录和重载。

本轮客户端未运行,当前会话没有 Minecraft MCP 工具,完整游戏压力测试仍待环境接入。PR 保持草稿;通过上述回归后再单独提交父整合包 CDC 升级和旧 HotAI 移除。审计、复测命令和回滚说明见 docs/dev-knowledge/kubejs-lazy.md

- 将 CDR #2309 的实例同步修复迁入 CDC,仅为 get/forget 添加同步标志。
- 保留上游方法体,增加模组门控、结构校验及补丁应用日志。
- 添加真实 KubeJS JAR 回归任务与后续游戏测试、HotAI 迁移文档。

验证:Java 17 完整构建通过;build.24 两轮各 200 万次并发回归中,
原版空值 83,275/63,875 次,转换后均为 0;缓存、过期及异常重试通过。
完整 Forge 加载、冷启动进档和独立服务器测试留待草稿 PR 后续验证。
- 审计 KubeJS 资源生成、脚本 API 和外部直接引用,区分内部竞态与 ProbeJS 调用。
- 用受控锁顺序夹具确认整方法同步能新增死锁,改用原子快照并在无缓存锁时执行 factory。
- forget 发布独立身份,阻止失效前的计算回填;保留合法 null、绝对过期和异常传播。
- 更新回归工具、兼容台账、并发语义和后续游戏测试说明。

验证:Java 17 完整构建和 reobf 通过;KubeJS build.16/24/26 各 200 万轮
并发回归补丁版均为零空值,缓存/过期/异常/失效中计算和锁顺序探针通过。
完整 Forge 冷启动、连续 reload 和独立服务器尚未执行,PR 继续保持草稿。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant