fix(ticket): guard DistanceManager ticket iteration with DR shared lock

问题:TicketScanService.visitTicketChunks 无锁遍历 DistanceManager.tickets
(Long2ObjectOpenHashMap,非线程安全)。DR 维度线程在 tick 中并发增删 ticket,
与主线程遍历冲突 -> fastutil fail-fast 迭代器 NPE(实测崩溃:
Long2ObjectOpenHashMap$MapIterator.nextEntry "this.wrapped is null")。

修法:新增 CrossModLocks 反射获取 DR 的共享锁
(dev.modzuozhi.core.compat.DistanceManagerLock.LOCK),遍历时持锁并在锁内
快照 entry 到局部 List,锁外再执行 visitor。DR 未加载则锁为 null,退化为
原无锁行为(向后兼容)。

- 新增 CrossModLocks(反射 + 缓存)
- TicketScanService 遍历加锁 + 快照
This commit is contained in:
Xiao-no-love committed 2026-10-13 17:02:08 +08:00
1 parent ed609f65d8
commit 3fbc956a62
2 files changed
+102 -7

No files matched your search

@@ -0,0 +1,60 @@
package com.moepus.serverwarashi.common.core.ticket;
import java.lang.reflect.Field;
import java.util.concurrent.locks.ReentrantLock;
/**
* 跨 mod 共享锁访问器。
* <p>
* DimensionalRipper 让维度 tick 跑在独立线程后,会并发修改 {@code DistanceManager.tickets}
* ({@code Long2ObjectOpenHashMap},非线程安全)。DR 用
* {@code dev.modzuozhi.core.compat.DistanceManagerLock.LOCK} 串行化所有 ticket 写路径。
* <p>
* warashi 的性能采样在主线程遍历同一张 ticket 表({@link TicketScanService}),必须持同一把锁,
* 否则遍历会与维度线程的写并发 → fastutil fail-fast 迭代器 NPE(已实测)。
* <p>
* <b>软依赖</b>:通过反射获取 DR 的锁;DR 未加载则 {@link #distanceManagerLock()} 返回 null,
* 调用方退化为无锁(原行为不变)。获取一次后缓存。
*/
public final class CrossModLocks {
private static final String DR_LOCK_CLASS =
"dev.modzuozhi.core.compat.DistanceManagerLock";
private static final String DR_LOCK_FIELD = "LOCK";
private static volatile ReentrantLock distanceManagerLock;
private static volatile boolean resolved;
private CrossModLocks() {
}
/**
* DR 的 DistanceManager 共享锁;DR 未加载时返回 null。
* 首次调用解析后缓存(含「解析失败」结果,避免反复抛异常)。
*/
public static ReentrantLock distanceManagerLock() {
if (!resolved) {
synchronized (CrossModLocks.class) {
if (!resolved) {
distanceManagerLock = resolve();
resolved = true;
}
}
}
return distanceManagerLock;
}
private static ReentrantLock resolve() {
try {
Class<?> lockClass = Class.forName(DR_LOCK_CLASS, false,
Thread.currentThread().getContextClassLoader());
Field field = lockClass.getField(DR_LOCK_FIELD);
Object value = field.get(null);
if (value instanceof ReentrantLock lock) {
return lock;
}
} catch (Throwable ignored) {
// DR 未加载 / 字段缺失:退化为无锁。
}
return null;
}
}
@@ -6,8 +6,18 @@ import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.Ticket;
import net.minecraft.util.SortedArraySet;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
/**
* 低分配遍历 DistanceManager 当前 ticket 表的访问器。
* <p>
* <b>并发安全</b>:{@code DistanceManager.tickets} 是非线程安全的
* {@code Long2ObjectOpenHashMap}。DimensionalRipper 维度线程会在 tick 中并发增删 ticket,
* 与主线程此处的遍历冲突 → fastutil fail-fast 迭代器 NPE(已实测)。
* 故遍历时持 DR 的共享锁({@link CrossModLocks}),并在锁内把 entry 快照到局部 List 再遍历,
* 避免持锁期间执行 visitor(可能较慢)。DR 未加载时锁为 null,退化为原无锁行为。
*/
public final class TicketScanService {
private TicketScanService() {
@@ -19,15 +29,40 @@ public final class TicketScanService {
}
public static void visitTicketChunks(DistanceManagerAccessor distanceManager, TicketChunkVisitor visitor) {
for (Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>> entry :
distanceManager.getTickets().long2ObjectEntrySet()) {
long chunkPos = entry.getLongKey();
SortedArraySet<Ticket<?>> tickets = entry.getValue();
if (tickets.isEmpty()) {
continue;
ReentrantLock lock = CrossModLocks.distanceManagerLock();
if (lock == null) {
// DR 未加载:原行为,直接遍历。
for (Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>> entry :
distanceManager.getTickets().long2ObjectEntrySet()) {
visitEntry(entry, visitor);
}
visitor.visitChunk(chunkPos, tickets);
return;
}
// DR 共存:锁内快照 entry,锁外遍历,与维度线程的 ticket 写互斥。
List<Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>>> snapshot;
lock.lock();
try {
snapshot = new ArrayList<>();
for (Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>> entry :
distanceManager.getTickets().long2ObjectEntrySet()) {
snapshot.add(entry);
}
} finally {
lock.unlock();
}
for (Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>> entry : snapshot) {
visitEntry(entry, visitor);
}
}
private static void visitEntry(Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>> entry,
TicketChunkVisitor visitor) {
long chunkPos = entry.getLongKey();
SortedArraySet<Ticket<?>> tickets = entry.getValue();
if (tickets.isEmpty()) {
return;
}
visitor.visitChunk(chunkPos, tickets);
}
@FunctionalInterface