feat: protect mod payloads with main-thread ownership in 1.1.17
This commit is contained in:
1 parent
691e949691
commit
763c015103
29 files changed
+1552
-53
No files matched your search
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user