From 87817d8adfe702a7e66c37ca392671126ae2eebf Mon Sep 17 00:00:00 2001 From: Matt <4009945+MattBDev@users.noreply.github.com> Date: Thu, 6 Aug 2026 13:20:44 -0400 Subject: [PATCH 1/2] Document the secondary pool's task contract The javadoc on QueueHandler#async described what the secondary pool is for, but not what a task submitted to it is required to avoid. Two rules were relied on implicitly and are now stated: - A task must not block waiting on a future that this pool completes. ForkJoinPool only compensates for blocking it observes through managedBlock, so monitorenter and Future#get leave a worker counted as running and no replacement is started. Callers with such a dependency should submit the dependent half separately and chain the futures rather than blocking inside one task. - Actor and command work does not belong here; Actor#runAction serialises per actor without holding a worker. Also notes that downstream plugins should use their own threads rather than this pool, which is sized for FAWE's own cleanup work. Documentation only; no behavioural change. --- .../queue/implementation/QueueHandler.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/queue/implementation/QueueHandler.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/queue/implementation/QueueHandler.java index e5f43ddbcb..e2416f3bbc 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/queue/implementation/QueueHandler.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/queue/implementation/QueueHandler.java @@ -63,6 +63,27 @@ public abstract class QueueHandler implements Trimable, Runnable { /** * Secondary queue should be used for "cleanup" tasks that are likely to be shorter in life than those submitted to the * primary queue. They may be IO-bound tasks. + * + *

+ * Tasks submitted here must observe two rules: + *

+ * + *

+ * Downstream plugins should not submit whole edits or other long-lived work to this pool, and should use their own + * threads instead. The pool is sized by {@code parallel-threads} for FAWE's own cleanup work; occupying it starves + * that work. + *

*/ private final ForkJoinPool forkJoinPoolSecondary = new ForkJoinPool( Settings.settings().QUEUE.PARALLEL_THREADS, @@ -195,6 +216,11 @@ public > void complete(Future task) { * Complete a task in the {@code forkJoinPoolSecondary} queue. Secondary queue should be used for "cleanup" tasks that are * likely to be shorter in life than those submitted to the primary queue. They may be IO-bound tasks. * + *

+ * The submitted task must not wait on a {@link Future} completed by this pool, and must not be actor or command work. + * See the {@code forkJoinPoolSecondary} field for the full contract and the reasoning behind it. + *

+ * * @param run Runnable to run * @param value Value to return when done * @param Value type @@ -208,6 +234,11 @@ public Future async(Runnable run, T value) { * Complete a task in the {@code forkJoinPoolSecondary} queue. Secondary queue should be used for "cleanup" tasks that are * likely to be shorter in life than those submitted to the primary queue. They may be IO-bound tasks. * + *

+ * The submitted task must not wait on a {@link Future} completed by this pool, and must not be actor or command work. + * See the {@code forkJoinPoolSecondary} field for the full contract and the reasoning behind it. + *

+ * * @param run Runnable to run * @return Future for submitted task */ @@ -219,6 +250,11 @@ public Future async(Runnable run) { * Complete a task in the {@code forkJoinPoolSecondary} queue. Secondary queue should be used for "cleanup" tasks that are * likely to be shorter in life than those submitted to the primary queue. They may be IO-bound tasks. * + *

+ * The submitted task must not wait on a {@link Future} completed by this pool, and must not be actor or command work. + * See the {@code forkJoinPoolSecondary} field for the full contract and the reasoning behind it. + *

+ * * @param call Callable to run * @param Return value type * @return Future for submitted task From 9fd54087e7ff191a3b5dc9c6203a139545a72e14 Mon Sep 17 00:00:00 2001 From: Matt <4009945+MattBDev@users.noreply.github.com> Date: Sun, 9 Aug 2026 22:22:10 -0400 Subject: [PATCH 2/2] Move secondary pool's task contract to a published location The full contract lived on the private forkJoinPoolSecondary field, which most Javadoc builds exclude, so the async(...) overloads' "See the field" pointer led nowhere for readers of the generated docs. Move the contract to the public getForkJoinPoolSecondary() accessor and have the field and the three async(...) overloads link to it with {@link}/@see instead of an unresolvable textual reference. --- .../queue/implementation/QueueHandler.java | 48 ++++++++++--------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/queue/implementation/QueueHandler.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/queue/implementation/QueueHandler.java index e2416f3bbc..9ff8cdcdb7 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/queue/implementation/QueueHandler.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/queue/implementation/QueueHandler.java @@ -64,26 +64,7 @@ public abstract class QueueHandler implements Trimable, Runnable { * Secondary queue should be used for "cleanup" tasks that are likely to be shorter in life than those submitted to the * primary queue. They may be IO-bound tasks. * - *

- * Tasks submitted here must observe two rules: - *

- *
    - *
  • A task must not wait on anything this pool completes. {@link ForkJoinPool} only compensates for - * blocking it can observe via {@link ForkJoinPool#managedBlock(ForkJoinPool.ManagedBlocker)}. It cannot see - * {@code monitorenter} or {@link Future#get()}, so it counts a blocked worker as running and does not start a - * replacement. Every worker can therefore end up parked waiting for work that only this pool can perform, and - * nothing progresses. If a task does have such a dependency, do not block inside it: submit the dependent half as - * a separate task and chain the futures.
  • - *
  • No actor or command work. Player-facing actions belong on - * {@link com.sk89q.worldedit.extension.platform.Actor#runAction(Runnable, boolean, boolean)} (or its - * {@code queueAction} / {@code runAsyncIfFree} wrappers), which serialises per actor without holding a worker - * here.
  • - *
- *

- * Downstream plugins should not submit whole edits or other long-lived work to this pool, and should use their own - * threads instead. The pool is sized by {@code parallel-threads} for FAWE's own cleanup work; occupying it starves - * that work. - *

+ * @see #getForkJoinPoolSecondary() the full task contract and the reasoning behind it */ private final ForkJoinPool forkJoinPoolSecondary = new ForkJoinPool( Settings.settings().QUEUE.PARALLEL_THREADS, @@ -218,7 +199,7 @@ public > void complete(Future task) { * *

* The submitted task must not wait on a {@link Future} completed by this pool, and must not be actor or command work. - * See the {@code forkJoinPoolSecondary} field for the full contract and the reasoning behind it. + * See {@link #getForkJoinPoolSecondary()} for the full contract and the reasoning behind it. *

* * @param run Runnable to run @@ -236,7 +217,7 @@ public Future async(Runnable run, T value) { * *

* The submitted task must not wait on a {@link Future} completed by this pool, and must not be actor or command work. - * See the {@code forkJoinPoolSecondary} field for the full contract and the reasoning behind it. + * See {@link #getForkJoinPoolSecondary()} for the full contract and the reasoning behind it. *

* * @param run Runnable to run @@ -252,7 +233,7 @@ public Future async(Runnable run) { * *

* The submitted task must not wait on a {@link Future} completed by this pool, and must not be actor or command work. - * See the {@code forkJoinPoolSecondary} field for the full contract and the reasoning behind it. + * See {@link #getForkJoinPoolSecondary()} for the full contract and the reasoning behind it. *

* * @param call Callable to run @@ -578,6 +559,27 @@ public ExecutorService getForkJoinPoolPrimary() { /** * Secondary queue should be used for "cleanup" tasks that are likely to be shorter in life than those submitted to the * primary queue. They may be IO-bound tasks. + * + *

+ * Tasks submitted here must observe two rules: + *

+ *
    + *
  • A task must not wait on anything this pool completes. {@link ForkJoinPool} only compensates for + * blocking it can observe via {@link ForkJoinPool#managedBlock(ForkJoinPool.ManagedBlocker)}. It cannot see + * {@code monitorenter} or {@link Future#get()}, so it counts a blocked worker as running and does not start a + * replacement. Every worker can therefore end up parked waiting for work that only this pool can perform, and + * nothing progresses. If a task does have such a dependency, do not block inside it: submit the dependent half as + * a separate task and chain the futures.
  • + *
  • No actor or command work. Player-facing actions belong on + * {@link com.sk89q.worldedit.extension.platform.Actor#runAction(Runnable, boolean, boolean)} (or its + * {@code queueAction} / {@code runAsyncIfFree} wrappers), which serialises per actor without holding a worker + * here.
  • + *
+ *

+ * Downstream plugins should not submit whole edits or other long-lived work to this pool, and should use their own + * threads instead. The pool is sized by {@code parallel-threads} for FAWE's own cleanup work; occupying it starves + * that work. + *

*

* Internal API usage only. *