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:
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
|
||||
|
||||
Reference in new issue
Block a user