feat: protect mod payloads with main-thread ownership in 1.1.17

This commit is contained in:
Disy committed 2026-10-06 17:36:00 +08:00
1 parent 691e949691
commit 763c015103
29 files changed
+1552 -53

No files matched your search

+200
View File
@@ -0,0 +1,200 @@
package dev.modzuozhi;
import dev.modzuozhi.core.dimthread.DimThreadCore;
import dev.modzuozhi.core.compat.SableMainThreadBridge;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.Blocks;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.neoforge.event.tick.ServerTickEvent;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
@EventBusSubscriber(modid="dimensionalripper")
public final class PayloadRegression {
static boolean ran;
static void check(boolean value, String reason) { if(!value) throw new AssertionError(reason); }
@SubscribeEvent
public static void tick(ServerTickEvent.Post event) throws Exception {
var server=event.getServer();
if(ran || server.getTickCount()<20) return;
ran=true;
if(Boolean.getBoolean("dimensionalripper.reviewRegression")) {
try {ReviewRegression.run(server);} finally {DimThreadCore.MANAGER.setPaused(server,true);server.halt(false);}
return;
}
var manager=DimThreadCore.MANAGER; var level=server.overworld();
var release=new CountDownLatch(1); var entered=new CountDownLatch(1);
var failure=new AtomicReference<Throwable>();
try {
String idleStats=dev.modzuozhi.core.compat.PayloadDiagnostics.snapshot(server);
for(int i=0;i<100;i++) dev.modzuozhi.core.compat.PayloadMainThreadDispatch.drain(server);
check(idleStats.equals(dev.modzuozhi.core.compat.PayloadDiagnostics.snapshot(server)),"empty drain created payload barrier/statistics");
ModZuozhi.LOGGER.info("PAYLOAD IDLE PASS: 100 empty drains created no payload batches");
manager.setPaused(server,true); level.setChunkForced(0,0,true); level.getChunk(0,0);
var pos=new BlockPos(8,115,8); level.setBlockAndUpdate(pos,Blocks.CHEST.defaultBlockState());
var chest=level.getBlockEntity(pos); check(chest!=null,"fixture chest");
manager.getLoop(level).execute(() -> {
server.execute(release::countDown); entered.countDown();
try {
check(release.await(5,TimeUnit.SECONDS),"callback not pumped");
SableMainThreadBridge.runForLevel(level, () -> check(level.getBlockEntity(pos)==chest,"handoff lookup"));
} catch(Throwable e) { failure.set(e); }
});
manager.setPaused(server,false); check(entered.await(5,TimeUnit.SECONDS),"worker start");
check(level.getBlockEntity(pos)==null,"must reproduce guarded null before phase");
manager.runMainThreadPhase(server,"payload-test",() -> {
check(server.isSameThread() && level.getBlockEntity(pos)==chest,"exclusive lookup");
manager.runMainThreadPhase(server,"nested",() -> check(level.getBlockEntity(pos)==chest,"nested lookup"));
// Regression: a normal resume during exclusive handling must not drop its lease.
manager.setPaused(server,false);
manager.getLoop(level).scheduleIfNeeded();
try { Thread.sleep(100); } catch(InterruptedException e) { throw new RuntimeException(e); }
check(!manager.getLoop(level).isTicking() && level.getBlockEntity(pos)==chest,"menu resume broke exclusive lease");
});
check(failure.get()==null,"Sable callback failed: "+failure.get());
manager.setPaused(server,true);
manager.runMainThreadPhase(server,"pre-paused",() -> {});
check(manager.getLoop(level).isPaused(),"pre-pause lost");
var expected=new IllegalStateException("sentinel");
try { manager.runMainThreadPhase(server,"failure",() -> {throw expected;}); throw new AssertionError("swallowed"); }
catch(IllegalStateException e) {check(e==expected,"exception identity");}
check(manager.getLoop(level).isPaused(),"exception lost pre-pause");
ModZuozhi.LOGGER.info("PAYLOAD PHASE PASS");
entry(server, pos, chest);
} finally { release.countDown(); manager.setPaused(server,true); server.halt(false); }
}
static void entry(net.minecraft.server.MinecraftServer server, BlockPos pos,
net.minecraft.world.level.block.entity.BlockEntity chest) throws Exception {
var manager=DimThreadCore.MANAGER; var level=server.overworld();
var player=new net.neoforged.neoforge.common.util.FakePlayer(level,
new com.mojang.authlib.GameProfile(java.util.UUID.randomUUID(), "PayloadTest"));
var ctx=new net.neoforged.neoforge.network.handling.ServerPayloadContext(player.connection,
net.minecraft.resources.ResourceLocation.parse("dimensionalripper:test"));
var entered=new CountDownLatch(1); var release=new CountDownLatch(1);
manager.getLoop(level).execute(() -> {
server.execute(release::countDown); entered.countDown();
try { check(release.await(5,TimeUnit.SECONDS),"entry callback pump"); }
catch(InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(e); }
});
manager.setPaused(server,false); check(entered.await(5,TimeUnit.SECONDS),"entry worker start");
try {
var future=ctx.enqueueWork(() -> {
check(server.isSameThread(),"payload ran off main");
check(level.getBlockEntity(pos)==chest,"public payload entry returned guarded null");
return ctx.enqueueWork(() -> 42).join();
});
check(future.join()==42,"nested supplier result");
var expected=new IllegalArgumentException("payload sentinel");
try { ctx.enqueueWork((Runnable) () -> { throw expected; }); throw new AssertionError("main exception hidden"); }
catch(IllegalArgumentException e) { check(e==expected,"main exception identity"); }
ModZuozhi.LOGGER.info("PAYLOAD ENTRY PASS");
asyncEntry(server,ctx,player);
manager.setPaused(server,false);
var assembled=new AtomicInteger();
dev.modzuozhi.core.compat.SableAssemblyDispatch.dispatch(player,() -> {
check(server.isSameThread(),"legacy assembly not main");
check(manager.loopsSnapshot().values().stream().noneMatch(loop -> loop.isTicking()),"legacy assembly lacks global ownership");
assembled.incrementAndGet();
});
check(assembled.get()==1,"legacy dispatch did not use common phase");
manager.setPaused(server,true);
ModInteractionRegression.run(server,ctx);
SimulatedHandleRegression.run(server,ctx);
var lifecyclePlayer=new net.neoforged.neoforge.common.util.FakePlayer(level,
new com.mojang.authlib.GameProfile(java.util.UUID.randomUUID(),"LifecycleTest"));
lifecycle(server,new net.neoforged.neoforge.network.handling.ServerPayloadContext(lifecyclePlayer.connection,ctx.payloadId()),lifecyclePlayer);
} finally { release.countDown(); manager.setPaused(server,true); }
}
static void asyncEntry(net.minecraft.server.MinecraftServer server,
net.neoforged.neoforge.network.handling.ServerPayloadContext ctx,
net.minecraft.server.level.ServerPlayer player) throws Exception {
var seen=new java.util.ArrayList<Integer>();
var fs=new java.util.ArrayList<CompletableFuture<Integer>>();
Thread producer=new Thread(() -> {
for(int i=0;i<100;i++) {int n=i; fs.add(ctx.enqueueWork(() -> {check(server.isSameThread(),"async handler main");seen.add(n);return n;}));}
},"payload-network-fixture");
producer.start(); producer.join(5000); check(!producer.isAlive(),"network submit blocks");
check(seen.isEmpty(),"off-thread handler executed inline");
while(fs.stream().anyMatch(f -> !f.isDone())) dev.modzuozhi.core.compat.PayloadMainThreadDispatch.drain(server);
check(seen.size()==100,"async handler count");
for(int i=0;i<100;i++) check(seen.get(i)==i && fs.get(i).join()==i,"async FIFO/result");
var cancelled=new AtomicReference<CompletableFuture<Integer>>();var changed=new AtomicInteger();
Thread cancelProducer=new Thread(() -> cancelled.set(ctx.enqueueWork(changed::incrementAndGet)));
cancelProducer.start();cancelProducer.join(5000);cancelled.get().cancel(false);
dev.modzuozhi.core.compat.PayloadMainThreadDispatch.drain(server);
check(changed.get()==0,"cancelled public future still mutated world");
var configListener=new net.minecraft.server.network.ServerConfigurationPacketListenerImpl(server,
new net.minecraft.network.Connection(net.minecraft.network.protocol.PacketFlow.SERVERBOUND),
net.minecraft.server.network.CommonListenerCookie.createInitial(player.getGameProfile(),false));
var configCtx=new net.neoforged.neoforge.network.handling.ServerPayloadContext(configListener,ctx.payloadId());
check(configCtx.enqueueWork(() -> 17).join()==17,"configuration phase not preserved");
var guarded=new AtomicReference<CompletableFuture<Integer>>();
Thread failing=new Thread(() -> guarded.set(ctx.enqueueWork(() -> {throw new IllegalArgumentException("expected test failure");})));
failing.start();failing.join(5000);dev.modzuozhi.core.compat.PayloadMainThreadDispatch.drain(server);
check(guarded.get().join()==null,"NeoForge async handler failure contract changed");
var raw=new AtomicReference<CompletableFuture<Integer>>();
Thread stale=new Thread(() -> raw.set(dev.modzuozhi.core.compat.PayloadMainThreadDispatch.submit(player,ctx.payloadId(),() -> {throw new AssertionError("stale handler executed");})));
stale.start();stale.join(5000);
var old=player.serverLevel(); player.setServerLevel(server.getLevel(net.minecraft.world.level.Level.NETHER));
dev.modzuozhi.core.compat.PayloadMainThreadDispatch.drain(server);
check(raw.get().isCompletedExceptionally(),"stale future pending"); player.setServerLevel(old);
check(dev.modzuozhi.core.compat.PayloadDiagnostics.snapshot(server).contains("expired=1"),"stale work not counted separately");
var nested=new AtomicReference<Throwable>(); var entered=new CountDownLatch(1);
var manager=DimThreadCore.MANAGER;
manager.setPaused(server,true);
manager.getLoop(old).execute(() -> {
entered.countDown();
SableMainThreadBridge.runForLevel(old,() -> {
try {ctx.enqueueWork(() -> 1); nested.set(new AssertionError("unsafe handoff reentry accepted"));}
catch(IllegalStateException expected) {}
});
});
manager.setPaused(server,false);check(entered.await(5,TimeUnit.SECONDS),"handoff fixture start");
manager.runMainThreadPhase(server,"handoff-test",() -> {});
check(nested.get()==null,"handoff reentry result");
manager.setPaused(server,true);
var blockedFuture=new AtomicReference<CompletableFuture<Integer>>();var waitDetected=new AtomicBoolean();
var waiting=new CountDownLatch(1);
manager.getLoop(old).execute(() -> {
CompletableFuture<Integer> f=dev.modzuozhi.core.compat.PayloadMainThreadDispatch.submit(player,ctx.payloadId(),() -> {throw new AssertionError("cancelled worker wait action ran");});
blockedFuture.set(f);waiting.countDown();
try { f.get(250,TimeUnit.MILLISECONDS); }
catch(TimeoutException expected) {waitDetected.set(true);f.cancel(false);}
catch(Exception e) {throw new RuntimeException(e);}
});
manager.setPaused(server,false);check(waiting.await(5,TimeUnit.SECONDS),"worker wait fixture start");
manager.runMainThreadPhase(server,"unsupported-worker-wait",() -> {});
check(waitDetected.get() && blockedFuture.get().isCancelled(),"worker blocking cycle not detected");
manager.setPaused(server,true);
ModZuozhi.LOGGER.info("PAYLOAD ASYNC PASS");
}
static void lifecycle(net.minecraft.server.MinecraftServer server,
net.neoforged.neoforge.network.handling.ServerPayloadContext ctx,
net.minecraft.server.level.ServerPlayer player) throws Exception {
var manager=DimThreadCore.MANAGER; var other=server.getLevel(net.minecraft.world.level.Level.NETHER);
manager.setPaused(server,true); manager.removeLoop(other);
manager.runMainThreadPhase(server,"new-loop",() -> {
var created=manager.getLoop(other);created.scheduleIfNeeded();
check(created.isPaused() && !created.isTicking(),"new loop escaped phase");
});
manager.setPaused(server,true);
// Bounded ID labels, not an unbounded record for every packet.
for(int i=0;i<1000;i++) {
dev.modzuozhi.core.compat.PayloadMainThreadDispatch.submit(player,
net.minecraft.resources.ResourceLocation.parse("dimensionalripper:test_"+i),() -> 1).join();
}
String stats=dev.modzuozhi.core.compat.PayloadDiagnostics.snapshot(server);
check(stats.contains("ids=128") && stats.contains("other="),"diagnostics ID cap: "+stats);
check(stats.contains("pending=0"),"diagnostic pending count: "+stats);
var pending=new AtomicReference<CompletableFuture<Integer>>();
Thread t=new Thread(() -> pending.set(dev.modzuozhi.core.compat.PayloadMainThreadDispatch.submit(player,ctx.payloadId(),() -> {throw new AssertionError("work after stop");})));
t.start();t.join(5000);
manager.runMainThreadPhase(server,"stop-inside-phase",() -> dev.modzuozhi.core.compat.PayloadMainThreadDispatch.stop(server));
check(pending.get().isDone() && pending.get().isCompletedExceptionally(),"stop leaves future pending");
check(manager.getLoop(other).isPaused(),"stop restored worker");
check(dev.modzuozhi.core.compat.PayloadMainThreadDispatch.submit(player,ctx.payloadId(),() -> 1).isCompletedExceptionally(),"stop accepts new work");
ModZuozhi.LOGGER.info("PAYLOAD LIFECYCLE PASS");
ModZuozhi.LOGGER.info("PAYLOAD DIAGNOSTICS PASS");
}
}