From 9d4a21de8f9455329d41d35f015fda1021506c0c Mon Sep 17 00:00:00 2001 From: mliem2k Date: Thu, 16 Jul 2026 10:06:27 +0800 Subject: [PATCH] Fix #49: synchronize Connection.flushQueue against concurrent region threads Paper's flushQueue() skips synchronization when isMainThread() is true, assuming only one thread can ever be "main". ShreddedPaperTickThread extends TickThread, so every region-tick-pool thread also satisfies isMainThread(). A player's flushQueue() can therefore be reached concurrently from Connection.tick() (the owning region thread), send() called from an async plugin task, and flushQueueInParallel()'s own sweep, none of which coordinate with each other, racing on the same pendingActions queue and reordering packets. Always synchronize on pendingActions so it is only ever drained by one thread at a time. --- .../minecraft/network/Connection.java.patch | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/shreddedpaper-server/minecraft-patches/sources/net/minecraft/network/Connection.java.patch b/shreddedpaper-server/minecraft-patches/sources/net/minecraft/network/Connection.java.patch index 7c7327f..6b741bf 100644 --- a/shreddedpaper-server/minecraft-patches/sources/net/minecraft/network/Connection.java.patch +++ b/shreddedpaper-server/minecraft-patches/sources/net/minecraft/network/Connection.java.patch @@ -62,7 +62,7 @@ this.channel.eventLoop().execute(() -> this.doSendPacket(packet, listener, flush)); } } -@@ -499,7 +_,7 @@ +@@ -499,19 +_,18 @@ } // Paper start - Optimize network: Rewrite this to be safer if ran off main thread @@ -71,6 +71,24 @@ if (!this.isConnected()) { return true; } +- if (io.papermc.paper.util.MCUtil.isMainThread()) { ++ // ShreddedPaper start - Fix packet order: every ShreddedPaperTickThread satisfies isMainThread(), so unlike ++ // vanilla's single main thread, multiple region-tick-pool threads can reach flushQueue() for the same ++ // connection at once (tick(), send() from an async plugin call, and flushQueueInParallel's own sweep all ++ // race). Always synchronize so pendingActions is only ever drained by one thread at a time. ++ synchronized (this.pendingActions) { + return this.processQueue(); +- } else if (this.isPending) { +- // Should only happen during login/status stages +- synchronized (this.pendingActions) { +- return this.processQueue(); +- } + } +- return false; ++ // ShreddedPaper end - Fix packet order + } + + private boolean processQueue() { @@ -555,7 +_,8 @@ private static int currTick; // Paper - Buffer joins to world private static int tickSecond; // Purpur - Max joins per second