Skip to content

Isolate exceptions in processTrackQueueInParallel/flushQueueInParallel - #77

Open
mliem2k wants to merge 1 commit into
MultiPaper:ver/26.2from
mliem2k:fix/parallel-tick-exception-handling
Open

mliem2k wants to merge 1 commit into
MultiPaper:ver/26.2from
mliem2k:fix/parallel-tick-exception-handling

Conversation

@mliem2k

@mliem2k mliem2k commented Jul 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Found while investigating #49 (unrelated, reporting separately since it's a distinct bug).

tickRegion() isolates any exception to the single failing region: it logs and calls moonrise$setChunkSystemCrash(...), letting the rest of the tick continue. processTrackQueueInParallel() and flushQueueInParallel(), one method away in the same file, have no .exceptionally() at all.

Traced the future chain up through ServerChunkCache.tick() -> ServerLevel.tickAsync() -> MinecraftServer's per-level future.join() (unguarded by any try/catch at that point). An exception thrown here is not silently dropped: it propagates uncaught through join() and unwinds tickChildren before the other level futures are joined or the rest of the tick runs. A real example is #32: an NPE from processTrackQueue (entity removed concurrently, so moonrise$getChunkData() is null) surfaced as a CompletionException and crashed the tick loop. With process-track-queue-in-parallel: false, the same NPE is thrown inside _tickRegion and caught by tickRegion()'s handler, so the handling currently depends on the optimization flag.

This adds matching .exceptionally() handlers to both methods, so a failure here is logged with the failing world and goes through the same controlled chunk-system crash path as tickRegion() instead of unwinding join() directly. It does not replace the root-cause fix in #32; the flushQueueInParallel half is symmetry and defence.

Test plan

  • Builds cleanly (./gradlew :shreddedpaper-server:compileJava)

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.

@PureGero

Copy link
Copy Markdown
Contributor

Please provide an example of such an exception that had a much larger blast radius

@mliem2k
mliem2k force-pushed the fix/parallel-tick-exception-handling branch from 006536d to 51a2449 Compare November 8, 2026 00:29
@mliem2k

mliem2k commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor Author

Good question. Here is a concrete example from #32 (the 2500-bot run):

Description: Exception in server tick loop

java.util.concurrent.CompletionException: java.lang.NullPointerException: Cannot read field "nearbyPlayers" because the return value of "...moonrise$getChunkData()" is null
    at java.base/java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java:1807)
Caused by: java.lang.NullPointerException: ...
    at io.multipaper.shreddedpaper.threading.ShreddedPaperEntityTicker.processTrackQueue(ShreddedPaperEntityTicker.java:44)
    at io.multipaper.shreddedpaper.threading.ShreddedPaperChunkTicker.lambda$processTrackQueueInParallel$6(ShreddedPaperChunkTicker.java:71)

The exception comes from the CompletableFuture.runAsync task inside processTrackQueueInParallel, because the entity was removed between the region snapshot and the parallel processing. With process-track-queue-in-parallel: false, the same NPE is thrown inside _tickRegion and caught by tickRegion's .exceptionally(), so today the handling depends on the optimization flag.

You're right that my "blast radius" wording was too strong. tickRegion's handler also sets the chunk-system crash, so both paths end in a server crash. The practical difference is how it fails. Without the handler, future.join() in tickChildren throws before the other level futures are joined and before the rest of the tick (player list, connections, and the flushQueueInParallel stage) runs, and the crash report is a bare CompletionException. With the handler, the error is logged with the failing phase, the level future completes normally, all worlds finish ticking, and the crash is the deliberate Chunk system crash propagated to tick().

This isn't meant to replace the root-cause fix in #32; I kept it symmetrical with tickRegion rather than adding a null check. I don't have an observed crash for the flushQueueInParallel half, that part is mostly symmetry and defence. I've updated the description to drop the "much larger blast radius" and plugin-hook claims, rebased onto the current ver/26.2, and added the world name to both messages so the failure points at the affected level.

Let me know what you think.

…rallel

Unlike tickRegion, which catches exceptions and marks a chunk-system
crash, the parallel tracked-entity and packet-flush futures had no
.exceptionally() at all. An exception there propagates uncaught through
ServerChunkCache.tick -> ServerLevel.tickAsync -> MinecraftServer's
future.join(), unwinding tickChildren before the other level futures
are joined or the rest of the tick runs. A real case is MultiPaper#32, where an
NPE from processTrackQueue surfaced as a CompletionException and
crashed the tick loop.

Add matching .exceptionally() handlers so a failure in these paths is
logged at the source and goes through the same controlled chunk-system
crash path as tickRegion. Include the world name in both messages so
the failure points at the affected level.
@mliem2k
mliem2k force-pushed the fix/parallel-tick-exception-handling branch from 51a2449 to 1c60857 Compare November 8, 2026 00:34
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.

3 participants