Skip to content

Fix #49: synchronize Connection.flushQueue against concurrent region threads#75

Open
mliem2k wants to merge 1 commit into
MultiPaper:ver/26.2from
mliem2k:fix/issue-49-packet-order
Open

Fix #49: synchronize Connection.flushQueue against concurrent region threads#75
mliem2k wants to merge 1 commit into
MultiPaper:ver/26.2from
mliem2k:fix/issue-49-packet-order

Conversation

@mliem2k

@mliem2k mliem2k commented Jul 16, 2026

Copy link
Copy Markdown

Summary

Fixes #49. Traced the root cause: ShreddedPaperTickThread extends TickThread, so every region-tick-pool thread satisfies Paper's isMainThread() check (MCUtil.isMainThread() delegates to MinecraftServer.isSameThread(), which is patched to TickThread.isTickThread()).

Paper's original flushQueue() skips synchronization on the isMainThread() path, an assumption that's safe in vanilla (only one thread is ever "main") but broken here: a player's flushQueue() can be reached concurrently from

  • Connection.tick() on the region thread that owns the player,
  • send() called from an async plugin task on some other thread,
  • flushQueueInParallel()'s own sweep over all players on a pooled tick thread,

none of which coordinate with each other. All of these can independently satisfy isMainThread() and race on the same connection's pendingActions queue, reordering packets. This matches the reported symptom (DecentHolograms entity-spawn packet arriving after its metadata packet).

The reporter's own workaround (unconditionally synchronizing flushQueue()) is the right general shape; this PR keeps that approach but removes the now-inaccurate isMainThread()/isPending branching entirely, since neither branch is actually safe to leave unsynchronized under ShreddedPaper's threading model.

Test plan

  • Builds cleanly (./gradlew :shreddedpaper-server:compileJava)
  • Manual in-game verification with a plugin that depends on packet ordering (e.g. DecentHolograms, per the original report)

…nt 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.
Copilot AI review requested due to automatic review settings July 16, 2026 02:08

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Packet sending order is not preserved

2 participants