整合 Issue #703 稳定性修复并落实代码评审意见#722
Open
FxRayHughes wants to merge 40 commits into
Open
Conversation
…s-init fix(nms): 避免 Bukkit 反射引导跨平台崩溃
- 新增基于 classpath SQL 文件的迁移机制与历史表校验 - 将 migration 相关实现集中到独立包 - 补充迁移执行器与 PersistentContainer 端到端测试
非隔离模式(默认)下,kotlinx-coroutines 被重定位成版本键控的共享包 (kotlin<ver>x.coroutines<ver>)加载进各插件 PluginClassLoader;因包名跨插件相同, Paper 插件类加载器组使其跨插件共享、首加载者定义。Dispatchers / CoroutineExceptionHandler 的首次初始化走 ServiceLoader,对触发线程 / 类加载器上下文敏感——当首次触发落在 AsyncPlayerPreLogin("User Authenticator")这类敌对线程时(很多插件在登录时做异步取数), 初始化会非确定性失败(NoClassDefFoundError)并永久毒化共享类,导致此后所有 TabooLib 插件的协程全部不可用。仅在多个 TabooLib 插件共存时偶发、极难排查。 修复:RuntimeEnv.init()(插件加载阶段、运行于启动主线程)加载完协程后,立即用 Class.forName(name, true, loader) 强制这些类在主线程完成首次初始化。JVM 保证类只初始化 一次,敌对线程此后只复用、不再触发脆弱的首次初始化。预热为尽力而为,任何失败都被吞掉, 零行为回归;仅在声明了协程(KOTLIN_COROUTINES_VERSION != null)的插件生效, 不改变任何 API / 加载语义,无新增依赖。
补充当前协程版本内部异常处理实现类,并仅在实际命中类时输出预热调试信息。
…-load fix(env): 启动主线程预热协程运行时,修复多插件共存时登录偶发 NoClassDefFoundError 崩溃
- nameInSpigot/nameInMojang 对 isSynthetic 与 Lambda 合成类静默返回 null - 修复 nameInMojang 误写入 spigotNameCache
- 在 ServerLoadEvent 后幂等补扫本地服务端通道 - 多 TabooLib 插件共存时仅覆盖主注入器遗漏的通道
确保 Future 异常可观察,消除递归删除死锁并可靠关闭文件监听与加载资源。
确保链任务和重复调度在失败或取消时终止,避免 Future 与协程永久挂起。
确保动作异常与取消能够结束上下文并回收运行任务,同时序列化远程读取并将计分板更新切回平台线程。
保证事件监听、类访问器缓存与延迟集合并发安全,并让命令树只构建一次且隔离每次执行结果。
确保数据源、连接、客户端和线程资源在启动失败、重连及并发关服场景下可靠释放,并补充相关回归测试。
通过复合唯一索引、兼容迁移和原子 upsert 阻止重复记录,并收敛缓存延迟写与调度竞态以确保最新值最终落库。
保留声明集合语义并按数据库方言生成合法 SQL,同时确保 JEXL 配置变更后重建引擎。
避免无上下文同步任务错误落到全局区域线程,并让 UI、虚拟背包与 NMS 操作按玩家或区域线程执行;同时保证物品数量不足时不发生部分扣除。
依据上游 TabooLib#704 / TabooLib#712 / TabooLib#716 / TabooLib#717 的代码评审意见修正整合分支: 1. onQuit 回调在 Bukkit / Bungee / Velocity 三平台从未触发(TabooLib#716 🔴) quitCallback 原为实例字段,而事件处理中 new XxxPlayer(e.player) 构造的是 全新实例,其集合必然为空。改为 companion 级按 UUID 存放的回调表, 玩家退出时移除条目以避免泄漏,并对回调异常做隔离。 2. isOwnedByCurrentRegion() 非 Folia 语义反转导致 navigation 回归(TabooLib#712 / TabooLib#717 🔴) 非 Folia 服务端没有区域概念,恢复恒返回 true。此前改为 Bukkit.isPrimaryThread() 后,配合 callRegion 的 check(...),bukkit-navigation 的 15 处调用在异步线程上 由「能跑」变为抛 IllegalStateException,且波及普通 Paper 服务器。 3. Folia 上无上下文同步任务被直接拒绝导致 @schedule 静默失效(TabooLib#712 🔴) ClassVisitorHandler 捕获异常后仅打印堆栈,任务不会注册。改为回退到全局区域 调度器执行并在首次命中时警告一次,提示迁移到 Location.submit() / Entity.submit() / submitGlobal()。 4. FileWatcher 同目录监听器互相 cancel(TabooLib#704 🔴) Path.register 对同一目录返回同一个 WatchKey,原实现在替换/移除监听器时直接 cancel,会连带废掉同目录其余监听器。改为目录级引用计数,计数归零才 cancel。 5. deepDeleteAsync 遇到被占用文件即中止整棵树(TabooLib#704 🔴) 补充 visitFileFailed 覆写并对删除失败做局部吞掉,避免单个被锁文件导致其余 文件全部残留;同时从 ForkJoinPool.commonPool(daemon)切换到非 daemon 的 专用线程池,避免 JVM 在删除完成前退出。 同步更新 BukkitExecutorTest 以匹配新的调度语义。
依据 TabooLib#704 / TabooLib#714 / TabooLib#715 / TabooLib#716 评审意见继续修正: 1. Mojang 短名回落改为 O(1) 查表(TabooLib#714-1) translateMojangToRuntimeOrKeep 原先对 classMapSpigotToMojang.values(6000+ 条) 做线性扫描,而 translate 会被 ASM 对类中每个类型引用调用一次且该层无缓存。 改为 lazy 预建「短名唯一才收录」的索引:既得 O(1) 查找,又保留了原 singleOrNull 在短名冲突时「无法确定则不改动」的保守语义。 顺带移除同文件内多余的 extraTransformers import(TabooLib#714-3)。 2. 平台禁用流程不再向外抛异常(TabooLib#715-7 / TabooLib#716-5) AfyBrokerPlugin.disable 的同步分支原先通过泛型擦除 rethrow,会中断 AfyBroker 对后续插件的卸载;HytaleCommandSender.fireQuitCallbacks 同样会把回调异常抛给 平台事件系统。两处统一为「记录但不抛」,与各自的异步分支保持一致。 3. Hytale 命令派发失败不再完全静默(TabooLib#716-2) dispatchCommand 去掉 future.get() 是正确的(在命令处理线程等待命令处理 Future 会自死锁),但异常此前被完全吞掉、连日志都没有。现挂上 whenComplete 记录失败。 4. 退出会话终态不再被包装实例构造清除(TabooLib#716-4) activateQuitSession 原先在 HytalePlayer 的 init 块中调用,而 adaptPlayer 每次 都会构造新实例,玩家退出后再次构造会把「已完成」标记清掉,使之后注册的回调 永远等待。改为在 registerQuitCallback 依据在线状态判定是否为新会话。 5. FileWatcherTest 跨平台稳定性与单例污染(TabooLib#704-4 / TabooLib#704-5) absolutePath 在 Windows / macOS 上可能与 canonicalPath 不等,改用 canonicalFile 比较——该断言此前在本机即失败;同时移除对全局单例 FileWatcher.INSTANCE.release() 的调用,其 released 状态不可逆,会污染同一 JVM 内的后续测试。 新增「同目录多监听器互不干扰」用例,覆盖目录级引用计数的修复。 同步更新 HytaleCommandSenderTest 以匹配不再 rethrow 的新语义。
1. Message.isCompleted() 不再用异常做控制流(TabooLib#713-4) readValidated 每收到一个数据包都会调用一次 isCompleted(),原实现通过 捕获 validateCompleted 抛出的 IllegalStateException 返回 false, 1024 分包的消息会构造 1023 个随即丢弃的异常(栈填充是其中最贵的部分)。 拆分出等价的 checkCompleted 布尔检查,validateCompleted 仍用于 build() 的 前置校验以保留原有异常信息。 2. 修正 VelocityPlugin.e(ProxyShutdownEvent) 的注释(TabooLib#715-1) 该方法在迁移 @subscribe 后已不再被 Velocity 触发,原注释「保留旧同步入口」 容易被误读为它仍在关服流程中生效。改为说明其仅保留公开签名, 实际关服由 eAsync 处理。
依据 TabooLib#712 / TabooLib#713 / TabooLib#715 评审的可选建议,落实公开 API 层面的线程安全改进: 1. Porticus.missions 改用 ConcurrentHashMap<UUID, PorticusMission>(TabooLib#713-1) 令「同一 UID 只能有一个 pending 任务」成为结构约束,取代注册时的 O(n) 线性查重; 终态裁决改用 remove(uid, mission),其原子性同样保证响应/超时/发送失败三方竞争时 回调恰好执行一次。事件处理由遍历改为按 UID 直接定位。 基类新增 isPending() 供子类复用,替代原先的 missions.contains(this)。 同时修正 run() 中 start/started 的赋值时序,确保在入列之前完成。 2. sendBungeeMessage 增加 queue 参数(TabooLib#713-2) 目标子服无活跃连接时现在会抛出 IllegalStateException,这会打断「遍历 getServers() 向所有子服广播」的用法。新增可选 queue 参数让此类调用退回 静默排队的旧行为,默认仍为快速失败。 3. Folia.isFolia 补充 volatile 与写入约束说明(TabooLib#712-7) 该值在类初始化时探测一次即固定,运行期修改会让已提交与新提交的任务落在 不同调度体系上。保留字段可写以兼容既有访问形式,另提供标注为内部 API 的 setFolia 供测试切换环境。 4. registeredCommands 改用 CopyOnWriteArrayList(TabooLib#715-9) Bukkit 侧写入受 commandLock 保护但字段公开、外部读取不持锁;Velocity 侧 同为公开裸 ArrayList。两处均改为 CoW 列表。 顺带修正 VelocityCommand.unregisterCommands 只注销不清空的问题——重复调用 会对同一命令反复注销;unregisterCommand 现在也会同步移除记录。
依据 TabooLib#717 / TabooLib#718 评审意见修正: 1. Node.createHash 真正消除两类碰撞(TabooLib#717-1) PR 描述声称「修复节点坐标哈希」但函数本身未改动,只加了开放寻址探测。 实际存在两类真实碰撞:y 只保留 8 位使 1.18+ 的 -64..320 必然重叠; x 的第 8 位与 z<0 标志位重叠,createHash(128,5,1) == createHash(0,5,-32767)。 改为纯位域布局 [0,12)=y、[12,22)=x、[22,32)=z,三段均取补码低位—— 补码天然分离正负,两个符号标志位一并移除。 Node.hash 仍为公开 Int 字段,签名与类型未变,仅数值分布改变;该值只用于 单次寻路过程中的临时节点表,不跨版本持久化,故未改为 Long 键 (后者需改动 NodeReader.nodes 的公开类型,才是真正的 ABI 破坏)。 开放寻址保留但简化:32 位仍装不下完整 x/z 范围,探测必须留作兜底。 步长由 key*31+1 改为 key++,线性探测在 int 空间的周期是完整 2^32 而非 2^28; 失败上界改为 nodes.size + 1,把「2.68 亿次乘法空转」变为即时失败, 错误信息补上坐标便于排查。 2. isStandableAtRegion 的支撑高度判定过严(TabooLib#717-3) 原判定要求 below.y + getBlockHeight(below) 与整数 y 精确相等(1e-3 容差), 而 getBlockHeight 对半砖返回 0.5、雪层返回 0.9375,实际只有满方块能通过, 使路径平滑在含台阶/农田/雪层的地形中退化为无操作。 改为区间判定,正确接受半砖、农田、雪层,同时排除空气与被埋情形。 3. addSweepBoundaries 补充对角穿角采样(TabooLib#717-5) x/z 边界在对角移动时于同一 t 重合,去重后中点采样落在格子内部, 可能漏掉经典的对角穿墙。现对重合点额外在 t±offset 取样, offset 按线段长度归一化以保证世界距离恒定。 4. getStartAtRegion 不再回落世界底部(TabooLib#717-4) 整柱非实心时原先返回 minHeight(通常是基岩/虚空),改为保留实体当前 y。 5. Fluid 查询缓存(TabooLib#717-7) NodeReader 新增按坐标的 fluid 缓存(与既有 type 缓存同构), Fluid.getFluid 的 waterlogged 分支前置空气短路, 免掉纵向扫描循环中最常见路径上的 getBlockData() 调用。 6. TypeJson 统一到 typeName 判断(TabooLib#718-3) 本 PR 只把 translate 改为基于 parseJsonType,score 仍用全等比较、 gradient 仍用 startsWith,导致 type: score:xxx 不匹配—— 与已修复的 translate:1:Stone 是同类缺陷。现五个分支全部统一。 顺带修正 gradient 参数不足 2 个时 toGradientColor 内部除零的问题。 7. SnapshotHashMap 的序列化陷阱(TabooLib#718-1) 该类继承 HashMap 但所有状态存于内部快照,父类桶数组永远为空, 而 HashMap.writeObject 是 private 无法覆盖——序列化会静默得到空 map。 覆盖 writeReplace() 写出快照副本,并补充说明继承动机、JDK 升级需复查 新增默认方法;replaceWith 的注释「CAS」改为「原子引用替换」。
依据 TabooLib#709 / TabooLib#710 / TabooLib#711 评审意见修正: 1. setDelayed 延迟窗口内的写入会永久丢失(TabooLib#710-1,数据丢失) 延迟第一次真正生效后,容器释放与关服都不 flush。 DataContainer 新增 flush():把 deadline 未到的项立即置为可写并在当前线程 同步排空(不经 asyncExecutor——关服阶段调度器可能已拒绝任务)。 三个 release 入口改为 remove(...)?.flush(),用 remove 返回值避免并发重复 flush; 伴生对象新增 @awake(LifeCycle.DISABLE) 全量 flush。 2. checkUpdate 主线程 O(全部键) 遍历且 writeStates 只增不减(TabooLib#710-5) 新增 deadlineKeys 集合,checkUpdate 只遍历它;drainWrites 收尾时回收空闲状态。 状态回收引入「已回收状态被继续写入」的窗口,故加 discarded 标记与 withState 辅助函数,在锁内校验状态仍是映射中的当前实例,否则重取重试。 3. forcedSet(sync = true) 重复写库(TabooLib#710-4) 原先改内存的同时还排一次异步写库,同一值写两遍,关服阶段还可能抛 RejectedExecutionException。新增只改缓存的内部方法供其调用。 4. removeDuplicateRows 无重试导致加载失败(TabooLib#710-7) 移入 try 内与 createUniqueIndex 共用重试;同时修正「重复数据已清零就直接抛」 的判定——归并本身失败时会被误判为不可重试。migrateSQLite 同样补上重试。 5. upsertGeneric 在值未变化时误抛(TabooLib#710-10) MySQL 下 UPDATE 写入相同值 affected rows 为 0,会被判为失败。 改为约束冲突后以 get(user, key) != null 判定成功。 6. Lettuce pub/sub 未初始化时的错误信息(TabooLib#709-3) pubSubConnection 改异步连接后,fire-and-forget 调用 start() 后立即使用 会撞 UninitializedPropertyAccessException。三个访问点统一前置检查, 抛出明确提示(等待 start() 返回的 future 或改用 startSync())。 7. RedisDatabaseHandler 的 AutoCloseable 未接线(TabooLib#709-6) 实现了 close() 但框架内无处调用。仿同模块既有注册表登记实例, @awake(LifeCycle.DISABLE) 统一释放,close() 保持幂等。 8. Redis 配置层级修正的升级兼容(TabooLib#709-1) 本 PR 把 enable/table 的读取层级统一到 Database 子节点,但老配置写在根节点, 升级后会读到默认值、切换数据源、看到空数据。 新增回退读取:用 contains 区分「键不存在」与「显式为 false」,缺失时回退根节点 并打印一次弃用警告。注意 enable 回退时建库用的 section 必须整体跟着回退—— 否则 HostSQL 会对 host/user/password 全部落默认值,连到 localhost:3306 的 root/root,比不回退更糟。 9. SQLite upsert 收口到 ON CONFLICT DO UPDATE(TabooLib#711-1 / TabooLib#711-10) 省略冲突目标的写法要求 SQLite >= 3.35.0,而 TabooLib 不声明 sqlite-jdbc 运行时依赖、版本不可控,失败表现为用户服务器上的裸 SQLSyntaxError。 改为要求显式传入冲突字段,门槛降至 3.24.0,错误信息直接给出正确写法。 database-player 的 SQLite 分支同步从手写 INSERT OR REPLACE 改用该路径—— 前者是「删旧行再插新行」,会重置用户手工添加的额外列并推进 autoincrement。 10. junit 版本与根构建对齐(TabooLib#710-14) database-player 的 junit-jupiter 由 5.10.2 降至 5.8.1。
依据 TabooLib#709 / TabooLib#710 评审的可选建议: 1. Redis 连接与 connector 的所有权一致化(TabooLib#709-4) SingleRedisConnector.connection() 每次调用都新建包装对象,但它们共享 connector 持有的同一个 pool;任一连接 close() 都会关掉该 pool, 使其余连接立即失效。Cluster 侧同样——其 close() 直接调 connector.close()。 两处改为缓存并复用单一连接实例,令「一个 connector 对应一个连接」成为事实; 连接已关闭时才重建,Single 侧在重连后同步新的 pool 引用。 同时为两个连接类补上公开的 isClosed() 判定。 2. DataContainer.updateMap 标注废弃(TabooLib#710-6) 该字段的语义已反转:旧实现存入「当前时间 - 延迟」(恒已过期的时间点), 判断是否该写库要看它是否早于当前时间;新实现存入未来的 deadline, 判断条件正好相反。外部读取者会得出完全相反的结论,故标注 @deprecated 并说明改用 setDelayed 表达延迟写入意图。 3. DataContainer.save(key) 补充 KDoc(TabooLib#710-3) 该方法在键不存在于缓存时会删除数据库中对应的行(早期实现抛 NPE), 属于「缓存即真相」的有意设计,但原注释只写「保存指定键的值到数据库」, 未体现删除语义。
依据 TabooLib#704 / TabooLib#705 / TabooLib#711 评审的可选建议: 1. submit-chain 的链异常不再彻底静默(TabooLib#705-1) 由 launch 改为 async 后,异常存入 Deferred 不再经过 CoroutineExceptionHandler, 而 submitChain { } 的常见用法是 fire-and-forget、不持有返回的 future, 此时异常会完全消失、排查比修复前更难。现统一记录一次。 取舍已写入注释:CompletableFuture 无法探测异常是否已被消费, 自行处理异常的调用方会额外看到一条日志,相比静默丢失是可接受的代价。 2. convertValue 补充全局转换器查询(TabooLib#711-4) 字段级路径会查 ConverterRegistry,元素级不查,导致 List<UUID> / Map<String, UUID> 仍抛 InvalidValueException,而同类型的单值字段却正常—— 与本 PR「按声明泛型递归恢复」的目标不一致。 3. 元素级枚举转换遵循 @SpecEnum(TabooLib#711-5) 原先硬编码 EnumGetMethod.NAME_IGNORECASE,忽略字段上的 @SpecEnum, 使 @SpecEnum(ORDINAL) var modes: EnumSet<Mode> 的声明失效。 现将取值方式沿 convertCollection / convertMap / convertValue 调用链传递。 4. Throttle.Singleton / Debounce.Singleton 的状态重置(TabooLib#704-6) 两者的状态分别存于 lastExecuteTime 与 task 字段,而继承来的 clearAll() / removeKey() 操作的是父类映射表(Singleton 从不写入),调用毫无效果。 改为覆写,真正重置自身状态。 5. RepeatChainable 的竞态覆盖补注释(TabooLib#705-5) taskReference 赋值后的二次 cancel 检查是为覆盖「任务在赋值前已完成」的竞态, 看似冗余,补注释防止后续维护者误删。
依据 TabooLib#707 / TabooLib#712 / TabooLib#714 评审要求,把行为变更写进对应 API 的注释: 1. getClasses() 返回不可修改集合(TabooLib#707-2) 该集合全局缓存,允许外部增删会破坏其他访问者看到的类视图。 曾对返回值增删的调用方会收到 UnsupportedOperationException。 2. 命令树在注册时构建一次并复用(TabooLib#707-1) 签名未变但语义变了:literal(*运行时列表) 这类在构建期读取可变状态的写法, 配置热重载后不再自动反映新值(此前依赖「每次执行都重建」而能生效)。 动态内容应改用 dynamic { suggestion { ... } }。 3. openVirtualInventory 要求在持有查看者的线程上调用(TabooLib#712-3) 此前在非主线程调用会把事件 submit 出去、函数照常返回, 但内部需要发包并写入 playerRemoteInventoryMap,异步执行本就不安全。 4. takeItem / checkItem 的边界翻转(TabooLib#712-4) 原子化本身已在 PR 描述中说明,但两处边界未提: amount = 0 现在视为成功返回 true(早期返回 false),checkItem 同理。 5. 补回 Mojang Mapping 分支被删除的背景注释(TabooLib#714-5) 原注释「Spigot.Fullname 交给 Paper PluginRemapper 转译」被本 PR 推翻, 但未说明原因。现说明 PluginRemapper 只覆盖插件本体的类引用, TabooLib 运行期动态生成/转译的类不在其范围内。 6. 统一转译方法命名(TabooLib#714-4) translateMojangToSpigotOrKeepRuntime 与 translateMojangToRuntimeOrKeep 后缀顺序相反、读起来易混。新增 translateMojangToSpigotOrKeep 作为正名, 旧名保留为废弃转发以兼容既有调用方。
依据 TabooLib#715-5 / TabooLib#716-3 评审建议(评审明确「建议 TabooLib#720 整合时统一处理」): Velocity / Application / AfyBroker / Hytale 四个 executor 存在高度一致的重复实现—— NEW/RUNNING/STOPPED 状态机、pending/active 双集合、停止后拒绝提交、 failure 累加 + addSuppressed + rethrow、命名线程工厂、DISABLE 阶段注册停止任务。 TabooLib#715 与 TabooLib#716 合并后会同时存在四份。 在 common-platform-api 新增 PlatformExecutorSupport,抽出: 状态枚举、任务登记表、「取消先于绑定」竞态协调器、失败收集器、 命名线程工厂、幂等 PlatformTask 包装,以及统一三个平台各自 inline 函数的 runReportingFailure。删除 AfyBrokerExecutorLifecycle.kt(173 行并入基类)。 净减少 422 行。 四个平台各自只保留调度差异:Velocity 的 buildTask 三分支、Application 的 ScheduledExecutorService 直调、AfyBroker 的 once/repeated × sync/async 四路、 Hytale 的 SCHEDULED_EXECUTOR 与 ScheduledFuture。 API 兼容性: - 四个执行器的无参构造器签名不变,PlatformFactory 的 cls.newInstance() 反射路径不受影响。 - 基类刻意不实现 PlatformExecutor——PlatformFactory.inject() 通过 cls.interfaces 只识别直接实现的接口,若把接口挪到基类会导致平台服务注册失效。 四个执行器仍各自显式实现该接口。 - scheduledTask 公开字段、platformTask() 返回类型、*PlatformTask 嵌套类名均不变。 顺带统一 TabooLib#715-3 的写法不一致:registerLifeCycleTask 的两种调用形式 (TabooLib.registerLifeCycleTask 与 platform.function.registerLifeCycleTask) 现在只在基类中出现一次。
依据 TabooLib#706 评审意见修正(评审列为阻塞级,建议阻止合并): 1. 父子 frame 锁顺序反转导致主线程死锁(TabooLib#706-1) 本 PR 给 SimpleNamedFrame 的 run/process/resume/close 都加了 synchronized, 形成两条相反的加锁路径: A: terminate() -> rootFrame.close() 持父锁再取子锁 B: child.resume() -> parent.resume() 持子锁再取父锁 并发时死锁,表现为 reload / 禁用时服务器主线程冻结—— 用「服务器挂死」换掉了「脚本挂起」,后果反而更重。 2. 持锁调用第三方插件代码(TabooLib#706-2) synchronized process() 内直接调用 action.process(this), 动作内若再 newFrame().run() 即自锁,并放大问题 1 的触发窗口。 修法:改用无锁状态机,而非「把回调移出锁外」——后者需要在每个临界区 手工维护「锁内计算 / 锁外执行」的两段式结构,而 process 内有 6 个提前 return 分支,极易漏掉一处。 - 移除全部 frame 级 synchronized,现在没有任何路径在持锁时调用外部代码 - 引入 AtomicInteger 令牌与排水循环:已有线程在推进时,后来者只登记请求即返回, 由持令牌线程代为执行。动作按序串行的语义不变,且动作在 process 栈内同步完成 并回调 resume 时不再递归(旧代码靠可重入锁掩盖了这一点) - future 字段改为 AtomicReference,运行权抢占由 checkState 改为 compareAndSet, close 改为 getAndSet(null),无锁保证只关闭一次 - 子 frame 列表改为同步列表并在 close 前快照遍历,避免关闭期间子 frame 完成引发 CME - 补上两处原先被锁掩盖的竞态:process 登记 whenComplete 前二次确认未关闭, 否则取消动作 future 防泄漏;SimpleActionFrame.run 用占位 future 抢占运行权 3. ExitStatus 三种语义被统一当作成功(TabooLib#706-3) success() / paused() / cooldown() 不加区分地走 completeResult, 被 terminateScript() 强制终止的脚本会以成功状态完成,调用方拿不到中断信号。 现按 status.isRunning() 分流:正常结束走完成,暂停与冷却走 cancel(false)。 同时把退出状态检查从 while 条件移进循环体首部,让「循环中途被打断」与 「循环跑完后发现终态」两条路径走同一套分流——旧代码这两处一个不完成 future、 一个无条件当成功,行为不一致。 4. RemoteQuestReader 锁错对象(TabooLib#706-5) 12 个方法的 @synchronized 锁的是 Reader 实例,而真正共享的游标在 source 上, 两个 Reader 包同一 source 时完全不互斥。改为 synchronized(source)。 新增回归测试:动作执行时未持有父/子 frame 锁的断言、200 轮 terminate 与 子动作完成的并发竞争(直接覆盖 ABBA 路径,带超时兜底)、三种 ExitStatus 分流、 4 个 Reader 共享同一 source 的互斥性验证。
构建期 OOM 会在仓库根目录生成 java_pid*.hprof,体积可达数百 MB, 超过 GitHub 单文件 100 MB 限制,需避免被误提交。
…ation # Conflicts: # module/database/build.gradle.kts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
整合 Issue #703 稳定性修复并落实代码评审意见
本分支整合上游 #704–#720 全部开放 PR(#720 已含 #704–#719,另单独 cherry-pick 了 Jie-150 的 #714),并在此基础上逐条落实这批 PR 的代码评审意见。
原始 PR 的修复内容见各自描述,本文只说明评审后新增的改动与升级需要注意的行为变化。
一、修复评审发现的阻塞级问题
1.
onQuit回调在 Bukkit / Bungee / Velocity 三平台从未触发quitCallback是实例字段,而事件处理中new XxxPlayer(e.player)构造的是全新实例,其集合必然为空——onQuit()注册的回调在这三个平台上从来不会执行。#716 已用 companion 级表修好 Hytale,三个主力平台未同步。改为 companion 级按 UUID 存放的回调表,玩家退出时移除条目以避免泄漏,并对回调异常做隔离。
2.
isOwnedByCurrentRegion()非 Folia 语义反转(跨 PR 回归)#712 把非 Folia 下的返回值从恒
true改为Bukkit.isPrimaryThread(),配合同 PR 给callRegion新增的check(...),使 bukkit-navigation 的 15 处调用在异步线程上由「能跑」变为抛IllegalStateException——且波及普通 Paper 服务器,不只 Folia。而 #717 保留了全部这些调用,两个 PR 单独看都不暴露,合并后才显现。非 Folia 服务端没有区域概念,「当前线程是否拥有该位置」在语义上不适用,恢复恒返回
true。需要主线程检查请显式使用Bukkit.isPrimaryThread()。3. Folia 上
@Schedule静默失效BukkitExecutor.submit对「无上下文同步任务」直接抛错,而全局submit的默认参数正是now=false, async=false。ClassVisitorHandler捕获异常后只打印堆栈不中断,导致任务静默不注册——包括 TabooLib 自身的ClassVisitorSchedule与DataContainer.checkUpdate。与 #710 叠加后,Folia 上setDelayed永不落库。改为回退到全局区域调度器执行,并在首次命中时警告一次,提示迁移到
Location.submit()/Entity.submit()/submitGlobal()。4. Kether 父子 frame 的 ABBA 死锁
#706 给
SimpleNamedFrame的run/process/resume/close都加了synchronized,形成两条相反的加锁路径(terminate() → rootFrame.close()持父锁取子锁;child.resume() → parent.resume()持子锁取父锁),并发时死锁,表现为 reload / 禁用时主线程冻结。此外synchronized process()内直接调用action.process(this),属持锁调用第三方代码。改用无锁状态机:移除全部 frame 级
synchronized,引入令牌与排水循环保证串行推进,future改为AtomicReference由 CAS 抢占运行权。5.
setDelayed延迟窗口内的写入永久丢失延迟第一次真正生效后,容器释放与关服都不 flush。新增
flush()走同步路径排空(关服阶段调度器可能已拒绝任务),三个 release 入口与@Awake(LifeCycle.DISABLE)均接入。6.
FileWatcher同目录监听器互相 cancelPath.register对同一目录返回同一个WatchKey,原实现在替换 / 移除监听器时直接 cancel,会连带废掉同目录其余监听器。改为目录级引用计数。7.
deepDeleteAsync遇被占用文件即中止整棵树缺
visitFileFailed覆写且postVisitDirectory主动重抛,单个锁定文件会导致其余文件全部残留——与该 PR「支持 Windows 热重载」的目标直接冲突。补齐覆写,并从ForkJoinPool.commonPool(daemon)切换到非 daemon 专用线程池。8. 导航节点哈希碰撞(描述与实现不符)
#717 描述称「修复节点坐标哈希」,但
createHash未改动,只加了开放寻址。实际存在两类真实碰撞:y 仅 8 位使 1.18+ 的 −64..320 必然重叠;x 的第 8 位与z<0标志位重叠(createHash(128,5,1) == createHash(0,5,-32767))。改为纯位域布局,补码低位天然分离正负,两个符号标志位一并移除。开放寻址保留作兜底,探测步长改为线性以获得完整 2^32 周期,失败上界改为
size+1使其即时失败而非空转 2.68 亿次。二、兼容性与行为变化
升级前请确认以下几项:
配置
enable/table的读取层级已修正到Database子节点。为兼容老配置,键缺失时会回退读取根节点并打印一次弃用警告,回退时建库用的整个 section 一并回退(否则HostSQL会对 host/user/password 落默认值连到localhost:3306)。请尽快把配置迁移到新层级,回退逻辑将在未来版本移除。API 行为
openVirtualInventory要求在持有查看者的线程上调用IllegalStateException,请用openVirtualInventoryAsync()/openMenu()/runTask()takeItem/checkItem原子化amount = 0现在返回true(此前false)getClasses()返回不可修改集合UnsupportedOperationExceptionliteral(*运行时列表)这类构建期读取可变状态的写法,热重载后不再反映新值;动态内容请用dynamic { suggestion { ... } }PorticusMission.run()变为一次性IllegalStateException,请每次new新实例getServers()广播的用法请改用新增的sendBungeeMessage(server, queue = true, ...)terminateScript()终止的脚本以 cancelled 结束completeExceptionallycomplete()将静默失效@Schedule/@Awake(ACTIVE)submit会抛RejectedExecutionExceptionsubmit需注意onDuplicateKeyUpdate必须显式传冲突字段database-player的 SQLite 分支改用ON CONFLICT DO UPDATEINSERT OR REPLACE是删旧行再插新行,会重置用户手工添加的额外列DataContainer.updateMap语义反转并标注废弃DataContainer.save(key)在键不存在时删除数据库行unwrap"null"/"~"变 null、"''"变空串并做 unicode 解码,List<String>运行时可能含 nulltoObject两次不再翻倍connection()返回同一实例Velocity.e(ProxyShutdownEvent)已不被触发eAsync处理,该方法仅保留公开签名已知不变
Node.hash仍是公开Int字段,签名与类型未变,仅数值分布改变。该值只用于单次寻路的临时节点表,不跨版本持久化。PlatformFactory的反射实例化路径不受影响。三、其他改进
translate会被 ASM 对每个类型引用调用一次且该层无缓存);porticusisCompleted()去除异常控制流(1024 分包会构造 1023 个随即丢弃的带栈异常);导航新增按坐标的流体缓存。PlatformExecutorSupport,消除 Velocity / Application / AfyBroker / Hytale 四份重复的 executor 状态机,净减 422 行。基类刻意不实现PlatformExecutor——PlatformFactory.inject()通过cls.interfaces只识别直接实现的接口。submitChain { }的链异常不再彻底静默;Throttle.Singleton/Debounce.Singleton的clearAll()真正重置自身状态。四、验证
全量
./gradlew build通过(编译 + 测试 + Java 8 门禁)。新增回归测试覆盖:kether 的 ABBA 死锁路径(200 轮并发竞争,带超时兜底)与动作执行时不持锁的断言、
FileWatcher同目录监听器互不干扰、导航哈希在 −64..319 全高度无碰撞、DataContainer的 flush 落库与状态回收、SQLite 显式冲突字段的实跑 upsert。五、评审中确认无需改动的项
bukkit-nms-legacy对platform-bukkit-impl扩展函数的依赖可正常解析(已有compileOnly声明)。@JvmSynthetic与internal的组合不影响测试调用。getReceiver() instanceof ProxiedPlayer限制正确——sendData均经玩家连接,这是唯一的发送形态。