From 50ad6c313192e24bd4d3a4b67c766369d073c2b2 Mon Sep 17 00:00:00 2001 From: Spottedleaf Date: Thu, 23 Mar 2023 02:51:04 -0700 Subject: [PATCH] New scheduler API Now, entity/global/location schedulers implement a generic run, runDelayed, and runAtFixedRate methods that provide a ScheduledTask value that can be used to interact with the scheduled task. Add also an async task scheduler that implements the same methods, except the delays/periods are in time and not ticks, as the scheduler is independent of the server tick process. Additionally, throw on some unimplemented APIs now. --- patches/api/0002-Region-scheduler-API.patch | 356 ++++- ...to-be-explicitly-marked-as-Folia-sup.patch | 2 +- ...king-ownership-of-region-by-position.patch | 8 +- patches/server/0004-Threaded-Regions.patch | 1317 ++++++++++++++++- ...-getHandle-and-overrides-perform-thr.patch | 13 + ...0010-Disable-mid-tick-task-execution.patch | 4 +- ...edOperationException-for-broken-APIs.patch | 74 + 7 files changed, 1693 insertions(+), 81 deletions(-) create mode 100644 patches/server/0011-Throw-UnsupportedOperationException-for-broken-APIs.patch diff --git a/patches/api/0002-Region-scheduler-API.patch b/patches/api/0002-Region-scheduler-API.patch index 36b40a9..a947176 100644 --- a/patches/api/0002-Region-scheduler-API.patch +++ b/patches/api/0002-Region-scheduler-API.patch @@ -6,17 +6,74 @@ Subject: [PATCH] Region scheduler API Add both a location based scheduler, an entity based scheduler, and a global region scheduler. +diff --git a/src/main/java/io/papermc/paper/threadedregions/scheduler/AsyncScheduler.java b/src/main/java/io/papermc/paper/threadedregions/scheduler/AsyncScheduler.java +new file mode 100644 +index 0000000000000000000000000000000000000000..64d1fe385d30f1f5ab82d35fe66e268da13346b1 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/scheduler/AsyncScheduler.java +@@ -0,0 +1,50 @@ ++package io.papermc.paper.threadedregions.scheduler; ++ ++import org.bukkit.plugin.Plugin; ++import org.jetbrains.annotations.NotNull; ++import java.util.concurrent.TimeUnit; ++import java.util.function.Consumer; ++ ++/** ++ * Scheduler that may be used by plugins to schedule tasks to execute asynchronously from the server tick process. ++ */ ++public interface AsyncScheduler { ++ ++ /** ++ * Schedules the specified task to be executed asynchronously immediately. ++ * @param plugin Plugin which owns the specified task. ++ * @param task Specified task. ++ * @return The {@link ScheduledTask} that represents the scheduled task. ++ */ ++ public @NotNull ScheduledTask runNow(@NotNull Plugin plugin, @NotNull Consumer task); ++ ++ /** ++ * Schedules the specified task to be executed asynchronously after the time delay has passed. ++ * @param plugin Plugin which owns the specified task. ++ * @param task Specified task. ++ * @param delay The time delay to pass before the task should be executed. ++ * @param unit The time unit for the time delay. ++ * @return The {@link ScheduledTask} that represents the scheduled task. ++ */ ++ public @NotNull ScheduledTask runDelayed(@NotNull Plugin plugin, @NotNull Consumer task, long delay, ++ @NotNull TimeUnit unit); ++ ++ /** ++ * Schedules the specified task to be executed asynchronously after the initial delay has passed, ++ * and then periodically executed with the specified period. ++ * @param plugin Plugin which owns the specified task. ++ * @param task Specified task. ++ * @param initialDelay The time delay to pass before the first execution of the task. ++ * @param period The time between task executions after the first execution of the task. ++ * @param unit The time unit for the initial delay and period. ++ * @return The {@link ScheduledTask} that represents the scheduled task. ++ */ ++ public @NotNull ScheduledTask runAtFixedRate(@NotNull Plugin plugin, @NotNull Consumer task, ++ long initialDelay, long period, @NotNull TimeUnit unit); ++ ++ /** ++ * Attempts to cancel all tasks scheduled by the specified plugin. ++ * @param plugin Specified plugin. ++ */ ++ public void cancelTasks(@NotNull Plugin plugin); ++} diff --git a/src/main/java/io/papermc/paper/threadedregions/scheduler/EntityScheduler.java b/src/main/java/io/papermc/paper/threadedregions/scheduler/EntityScheduler.java new file mode 100644 -index 0000000000000000000000000000000000000000..4193b13f1f51c2fb8da76f3e03187d859eaa8e10 +index 0000000000000000000000000000000000000000..9c4ee07a86104f3601ba6d8a911197dbe1a17102 --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/scheduler/EntityScheduler.java -@@ -0,0 +1,47 @@ +@@ -0,0 +1,103 @@ +package io.papermc.paper.threadedregions.scheduler; + +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; ++import java.util.function.Consumer; + +/** + * An entity can move between worlds with an arbitrary tick delay, be temporarily removed @@ -55,20 +112,76 @@ index 0000000000000000000000000000000000000000..4193b13f1f51c2fb8da76f3e03187d85 + * will be invoked (but never both), or {@code false} indicating neither the run nor retired function will be invoked + * since the scheduler has been retired. + */ -+ public boolean execute(@NotNull final Plugin plugin, @NotNull final Runnable run, @Nullable final Runnable retired, -+ final long delay); ++ public boolean execute(@NotNull Plugin plugin, @NotNull Runnable run, @Nullable Runnable retired, long delay); + ++ /** ++ * Schedules a task to execute on the next tick. If the task failed to schedule because the scheduler is retired (entity ++ * removed), then returns {@code null}. Otherwise, either the task callback will be invoked after the specified delay, ++ * or the retired callback will be invoked if the scheduler is retired. ++ * Note that the retired callback is invoked in critical code, so it should not attempt to remove the entity, remove ++ * other entities, load chunks, load worlds, modify ticket levels, etc. ++ * ++ *

++ * It is guaranteed that the task and retired callback are invoked on the region which owns the entity. ++ *

++ * @param plugin The plugin that owns the task ++ * @param task The task to execute ++ * @param retired Retire callback to run if the entity is retired before the run callback can be invoked, may be null. ++ * @return The {@link ScheduledTask} that represents the scheduled task, or {@code null} if the entity has been removed. ++ */ ++ public @Nullable ScheduledTask run(@NotNull Plugin plugin, @NotNull Consumer task, ++ @Nullable Runnable retired); ++ ++ /** ++ * Schedules a task with the given delay. If the task failed to schedule because the scheduler is retired (entity ++ * removed), then returns {@code null}. Otherwise, either the task callback will be invoked after the specified delay, ++ * or the retired callback will be invoked if the scheduler is retired. ++ * Note that the retired callback is invoked in critical code, so it should not attempt to remove the entity, remove ++ * other entities, load chunks, load worlds, modify ticket levels, etc. ++ * ++ *

++ * It is guaranteed that the task and retired callback are invoked on the region which owns the entity. ++ *

++ * @param plugin The plugin that owns the task ++ * @param task The task to execute ++ * @param retired Retire callback to run if the entity is retired before the run callback can be invoked, may be null. ++ * @param delayTicks The delay, in ticks. ++ * @return The {@link ScheduledTask} that represents the scheduled task, or {@code null} if the entity has been removed. ++ */ ++ public @Nullable ScheduledTask runDelayed(@NotNull Plugin plugin, @NotNull Consumer task, ++ @Nullable Runnable retired, int delayTicks); ++ ++ /** ++ * Schedules a repeating task with the given delay and period. If the task failed to schedule because the scheduler ++ * is retired (entity removed), then returns {@code null}. Otherwise, either the task callback will be invoked after ++ * the specified delay, or the retired callback will be invoked if the scheduler is retired. ++ * Note that the retired callback is invoked in critical code, so it should not attempt to remove the entity, remove ++ * other entities, load chunks, load worlds, modify ticket levels, etc. ++ * ++ *

++ * It is guaranteed that the task and retired callback are invoked on the region which owns the entity. ++ *

++ * @param plugin The plugin that owns the task ++ * @param task The task to execute ++ * @param retired Retire callback to run if the entity is retired before the run callback can be invoked, may be null. ++ * @param initialDelayTicks The initial delay, in ticks. ++ * @param periodTicks The period, in ticks. ++ * @return The {@link ScheduledTask} that represents the scheduled task, or {@code null} if the entity has been removed. ++ */ ++ public @Nullable ScheduledTask runAtFixedRate(@NotNull Plugin plugin, @NotNull Consumer task, ++ @Nullable Runnable retired, int initialDelayTicks, int periodTicks); +} diff --git a/src/main/java/io/papermc/paper/threadedregions/scheduler/GlobalRegionScheduler.java b/src/main/java/io/papermc/paper/threadedregions/scheduler/GlobalRegionScheduler.java new file mode 100644 -index 0000000000000000000000000000000000000000..c13c89c87a4e6fd441cac8d3c8cd5b283915467f +index 0000000000000000000000000000000000000000..f2d2565d903af90f6909319c811a49162f972e27 --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/scheduler/GlobalRegionScheduler.java -@@ -0,0 +1,22 @@ +@@ -0,0 +1,57 @@ +package io.papermc.paper.threadedregions.scheduler; + +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; ++import java.util.function.Consumer; + +/** + * The global region task scheduler may be used to schedule tasks that will execute on the global region. @@ -84,21 +197,56 @@ index 0000000000000000000000000000000000000000..c13c89c87a4e6fd441cac8d3c8cd5b28 + * @param plugin The plugin that owns the task + * @param run The task to execute + */ -+ public void execute(@NotNull final Plugin plugin, @NotNull final Runnable run); ++ public void execute(@NotNull Plugin plugin, @NotNull Runnable run); + ++ /** ++ * Schedules a task to be executed on the global region on the next tick. ++ * @param plugin The plugin that owns the task ++ * @param task The task to execute ++ * @return The {@link ScheduledTask} that represents the scheduled task. ++ */ ++ public @NotNull ScheduledTask run(@NotNull Plugin plugin, @NotNull Consumer task); ++ ++ /** ++ * Schedules a task to be executed on the global region after the specified delay in ticks. ++ * @param plugin The plugin that owns the task ++ * @param task The task to execute ++ * @param delayTicks The delay, in ticks. ++ * @return The {@link ScheduledTask} that represents the scheduled task. ++ */ ++ public @NotNull ScheduledTask runDelayed(@NotNull Plugin plugin, @NotNull Consumer task, int delayTicks); ++ ++ /** ++ * Schedules a repeating task to be executed on the global region after the initial delay with the ++ * specified period. ++ * @param plugin The plugin that owns the task ++ * @param task The task to execute ++ * @param initialDelayTicks The initial delay, in ticks. ++ * @param periodTicks The period, in ticks. ++ * @return The {@link ScheduledTask} that represents the scheduled task. ++ */ ++ public @NotNull ScheduledTask runAtFixedRate(@NotNull Plugin plugin, @NotNull Consumer task, ++ int initialDelayTicks, int periodTicks); ++ ++ /** ++ * Attempts to cancel all tasks scheduled by the specified plugin. ++ * @param plugin Specified plugin. ++ */ ++ public void cancelTasks(@NotNull Plugin plugin); +} diff --git a/src/main/java/io/papermc/paper/threadedregions/scheduler/RegionisedScheduler.java b/src/main/java/io/papermc/paper/threadedregions/scheduler/RegionisedScheduler.java new file mode 100644 -index 0000000000000000000000000000000000000000..210a3dce74959efd7ac0ca9a92a2ad8815844246 +index 0000000000000000000000000000000000000000..4912d47e3daa4071bc82b1a32a19c8ea3348e0cc --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/scheduler/RegionisedScheduler.java -@@ -0,0 +1,26 @@ +@@ -0,0 +1,60 @@ +package io.papermc.paper.threadedregions.scheduler; + +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; ++import java.util.function.Consumer; + +/** + * The region task scheduler can be used to schedule tasks by location to be executed on the region which owns the location. @@ -117,13 +265,164 @@ index 0000000000000000000000000000000000000000..210a3dce74959efd7ac0ca9a92a2ad88 + * @param location The location at which the region executing should own + * @param run The task to execute + */ -+ public void execute(@NotNull final Plugin plugin, @NotNull final Location location, @NotNull final Runnable run); ++ public void execute(@NotNull Plugin plugin, @NotNull Location location, @NotNull Runnable run); ++ ++ /** ++ * Schedules a task to be executed on the region which owns the location on the next tick. ++ * @param plugin The plugin that owns the task ++ * @param location The location at which the region executing should own ++ * @param task The task to execute ++ * @return The {@link ScheduledTask} that represents the scheduled task. ++ */ ++ public @NotNull ScheduledTask run(@NotNull Plugin plugin, @NotNull Location location, @NotNull Consumer task); ++ ++ /** ++ * Schedules a task to be executed on the region which owns the location after the specified delay in ticks. ++ * @param plugin The plugin that owns the task ++ * @param location The location at which the region executing should own ++ * @param task The task to execute ++ * @param delayTicks The delay, in ticks. ++ * @return The {@link ScheduledTask} that represents the scheduled task. ++ */ ++ public @NotNull ScheduledTask runDelayed(@NotNull Plugin plugin, @NotNull Location location, @NotNull Consumer task, ++ int delayTicks); ++ ++ /** ++ * Schedules a repeating task to be executed on the region which owns the location after the initial delay with the ++ * specified period. ++ * @param plugin The plugin that owns the task ++ * @param location The location at which the region executing should own ++ * @param task The task to execute ++ * @param initialDelayTicks The initial delay, in ticks. ++ * @param periodTicks The period, in ticks. ++ * @return The {@link ScheduledTask} that represents the scheduled task. ++ */ ++ public @NotNull ScheduledTask runAtFixedRate(@NotNull Plugin plugin, @NotNull Location location, @NotNull Consumer task, ++ int initialDelayTicks, int periodTicks); ++} +diff --git a/src/main/java/io/papermc/paper/threadedregions/scheduler/ScheduledTask.java b/src/main/java/io/papermc/paper/threadedregions/scheduler/ScheduledTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..fa4ac300d3721b2d6d84b95618d3305874cb803d +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/scheduler/ScheduledTask.java +@@ -0,0 +1,112 @@ ++package io.papermc.paper.threadedregions.scheduler; ++ ++import org.bukkit.plugin.Plugin; ++import org.jetbrains.annotations.NotNull; ++ ++/** ++ * Represents a task scheduled to a scheduler. ++ */ ++public interface ScheduledTask { ++ ++ /** ++ * Returns the plugin that scheduled this task. ++ * @return the plugin that scheduled this task. ++ */ ++ public @NotNull Plugin getOwningPlugin(); ++ ++ /** ++ * Returns whether this task executes on a fixed period, as opposed to executing only once. ++ * @return whether this task executes on a fixed period, as opposed to executing only once. ++ */ ++ public boolean isRepeatingTask(); ++ ++ /** ++ * Attempts to cancel this task, returning the result of the attempt. In all cases, if the task is currently ++ * being executed no attempt is made to halt the task, however any executions in the future are halted. ++ * @return the result of the cancellation attempt. ++ */ ++ public @NotNull CancelledState cancel(); ++ ++ /** ++ * Returns the current execution state of this task. ++ * @return the current execution state of this task. ++ */ ++ public @NotNull ExecutionState getExecutionState(); ++ ++ /** ++ * Returns whether the current execution state is {@link ExecutionState#CANCELLED} or {@link ExecutionState#CANCELLED_RUNNING}. ++ * @return whether the current execution state is {@link ExecutionState#CANCELLED} or {@link ExecutionState#CANCELLED_RUNNING}. ++ */ ++ public default boolean isCancelled() { ++ final ExecutionState state = this.getExecutionState(); ++ return state == ExecutionState.CANCELLED || state == ExecutionState.CANCELLED_RUNNING; ++ } ++ ++ /** ++ * Represents the result of attempting to cancel a task. ++ */ ++ public enum CancelledState { ++ /** ++ * The task (repeating or not) has been successfully cancelled by the caller thread. The task is not executing ++ * currently, and it will not begin execution in the future. ++ */ ++ CANCELLED_BY_CALLER, ++ /** ++ * The task (repeating or not) is already cancelled. The task is not executing currently, and it will not ++ * begin execution in the future. ++ */ ++ CANCELLED_ALREADY, ++ ++ /** ++ * The task is not a repeating task, and could not be cancelled because the task is being executed. ++ */ ++ RUNNING, ++ /** ++ * The task is not a repeating task, and could not be cancelled because the task has already finished execution. ++ */ ++ ALREADY_EXECUTED, ++ ++ /** ++ * The caller thread successfully stopped future executions of a repeating task, but the task is currently ++ * being executed. ++ */ ++ NEXT_RUNS_CANCELLED, ++ ++ /** ++ * The repeating task's future executions are cancelled already, but the task is currently ++ * being executed. ++ */ ++ NEXT_RUNS_CANCELLED_ALREADY, ++ } ++ ++ /** ++ * Represents the current execution state of the task. ++ */ ++ public enum ExecutionState { ++ /** ++ * The task is currently not executing, but may begin execution in the future. ++ */ ++ IDLE, ++ ++ /** ++ * The task is currently executing. ++ */ ++ RUNNING, ++ ++ /** ++ * The task is not repeating, and the task finished executing. ++ */ ++ FINISHED, ++ ++ /** ++ * The task is not executing and will not begin execution in the future. If this task is not repeating, then ++ * this task was never executed. ++ */ ++ CANCELLED, ++ ++ /** ++ * The task is repeating and currently executing, but future executions are cancelled and will not occur. ++ */ ++ CANCELLED_RUNNING; ++ } +} diff --git a/src/main/java/org/bukkit/Bukkit.java b/src/main/java/org/bukkit/Bukkit.java -index ac9b690fcccb60b587e5345f12f1383afd0a73a1..4ae25cb199c6e20b51c4a1e7ac4ff55b23ae724f 100644 +index ac9b690fcccb60b587e5345f12f1383afd0a73a1..22952628c894e29bfdb94897bd9970103730b898 100644 --- a/src/main/java/org/bukkit/Bukkit.java +++ b/src/main/java/org/bukkit/Bukkit.java -@@ -2459,6 +2459,35 @@ public final class Bukkit { +@@ -2459,6 +2459,44 @@ public final class Bukkit { return server.getPotionBrewer(); } // Paper end @@ -144,6 +443,15 @@ index ac9b690fcccb60b587e5345f12f1383afd0a73a1..4ae25cb199c6e20b51c4a1e7ac4ff55b + } + + /** ++ * Returns the async task scheduler. The async task scheduler can be used to schedule tasks ++ * that execute asynchronously from the server tick process. ++ * @return the async task scheduler ++ */ ++ public static @NotNull io.papermc.paper.threadedregions.scheduler.AsyncScheduler getAsyncScheduler() { ++ return server.getAsyncScheduler(); ++ } ++ ++ /** + * Returns the global region task scheduler. The global task scheduler can be used to schedule + * tasks to execute on the global region. + *

@@ -160,10 +468,10 @@ index ac9b690fcccb60b587e5345f12f1383afd0a73a1..4ae25cb199c6e20b51c4a1e7ac4ff55b @NotNull public static Server.Spigot spigot() { diff --git a/src/main/java/org/bukkit/Server.java b/src/main/java/org/bukkit/Server.java -index 2204336d8800311b65e894739ab1b27273e7c6f2..1092ceef77bad421df647d349d997d02b2ba80a9 100644 +index 2204336d8800311b65e894739ab1b27273e7c6f2..ea4d93680066295de9fd447eda58b93014eac635 100644 --- a/src/main/java/org/bukkit/Server.java +++ b/src/main/java/org/bukkit/Server.java -@@ -2139,4 +2139,29 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi +@@ -2139,4 +2139,36 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi */ @NotNull org.bukkit.potion.PotionBrewer getPotionBrewer(); // Paper end @@ -179,7 +487,14 @@ index 2204336d8800311b65e894739ab1b27273e7c6f2..1092ceef77bad421df647d349d997d02 + *

+ * @return the region task scheduler + */ -+ @NotNull io.papermc.paper.threadedregions.scheduler.RegionisedScheduler getRegionScheduler(); ++ public @NotNull io.papermc.paper.threadedregions.scheduler.RegionisedScheduler getRegionScheduler(); ++ ++ /** ++ * Returns the async task scheduler. The async task scheduler can be used to schedule tasks ++ * that execute asynchronously from the server tick process. ++ * @return the async task scheduler ++ */ ++ public @NotNull io.papermc.paper.threadedregions.scheduler.AsyncScheduler getAsyncScheduler(); + + /** + * Returns the global region task scheduler. The global task scheduler can be used to schedule @@ -212,15 +527,18 @@ index cdbc7329cf5f67d66e31eb31e83b9e7997040f72..90451ed12b2c95bb372ac2e3cbb57b8b + // Folia end - region threading API } diff --git a/src/main/java/org/bukkit/plugin/SimplePluginManager.java b/src/main/java/org/bukkit/plugin/SimplePluginManager.java -index b012ce40d82389c29d1b841ff685425ac10a7f9e..499dc8309a16b33d16b57b433c3c5b4330323717 100644 +index b012ce40d82389c29d1b841ff685425ac10a7f9e..057fa5dc78734520224af5031250b6be101ce3cb 100644 --- a/src/main/java/org/bukkit/plugin/SimplePluginManager.java +++ b/src/main/java/org/bukkit/plugin/SimplePluginManager.java -@@ -585,7 +585,7 @@ public final class SimplePluginManager implements PluginManager { +@@ -585,9 +585,9 @@ public final class SimplePluginManager implements PluginManager { } try { - server.getScheduler().cancelTasks(plugin); -+ //server.getScheduler().cancelTasks(plugin); // Folia - Bukkit scheduler not supported ++ server.getAsyncScheduler().cancelTasks(plugin); // Folia - new schedulers } catch (Throwable ex) { - handlePluginException("Error occurred (in the plugin loader) while cancelling tasks for " +- handlePluginException("Error occurred (in the plugin loader) while cancelling tasks for " ++ handlePluginException("Error occurred (in the plugin loader) while cancelling async tasks for " // Folia - new schedulers + plugin.getDescription().getFullName() + " (Is it up to date?)", ex, plugin); // Paper + } + diff --git a/patches/api/0003-Require-plugins-to-be-explicitly-marked-as-Folia-sup.patch b/patches/api/0003-Require-plugins-to-be-explicitly-marked-as-Folia-sup.patch index 8832429..26c6db2 100644 --- a/patches/api/0003-Require-plugins-to-be-explicitly-marked-as-Folia-sup.patch +++ b/patches/api/0003-Require-plugins-to-be-explicitly-marked-as-Folia-sup.patch @@ -48,7 +48,7 @@ index 0c9f4d1e9104fa6951114c1f9ec954dfcc749196..fc11577083672f127335613459436167 @NotNull diff --git a/src/main/java/org/bukkit/plugin/SimplePluginManager.java b/src/main/java/org/bukkit/plugin/SimplePluginManager.java -index 499dc8309a16b33d16b57b433c3c5b4330323717..25f228e616d1f5475a06bf3eeb2cf1cf6b6ed352 100644 +index 057fa5dc78734520224af5031250b6be101ce3cb..ec838d1c7850f0d07980b051dff7c88db0aa6dbf 100644 --- a/src/main/java/org/bukkit/plugin/SimplePluginManager.java +++ b/src/main/java/org/bukkit/plugin/SimplePluginManager.java @@ -163,6 +163,12 @@ public final class SimplePluginManager implements PluginManager { diff --git a/patches/api/0005-Add-API-for-checking-ownership-of-region-by-position.patch b/patches/api/0005-Add-API-for-checking-ownership-of-region-by-position.patch index 6d10adc..d9e5b67 100644 --- a/patches/api/0005-Add-API-for-checking-ownership-of-region-by-position.patch +++ b/patches/api/0005-Add-API-for-checking-ownership-of-region-by-position.patch @@ -11,10 +11,10 @@ the schedulers depending on the result of the ownership check. diff --git a/src/main/java/org/bukkit/Bukkit.java b/src/main/java/org/bukkit/Bukkit.java -index 4ae25cb199c6e20b51c4a1e7ac4ff55b23ae724f..b8c62f431bf802a80fa097f1e5f8e6247badd0ea 100644 +index 22952628c894e29bfdb94897bd9970103730b898..62b7f94de5ffacd3919f9843e64a40f6f329fadd 100644 --- a/src/main/java/org/bukkit/Bukkit.java +++ b/src/main/java/org/bukkit/Bukkit.java -@@ -2487,6 +2487,100 @@ public final class Bukkit { +@@ -2496,6 +2496,100 @@ public final class Bukkit { public static @NotNull io.papermc.paper.threadedregions.scheduler.GlobalRegionScheduler getGlobalRegionScheduler() { return server.getGlobalRegionScheduler(); } @@ -116,10 +116,10 @@ index 4ae25cb199c6e20b51c4a1e7ac4ff55b23ae724f..b8c62f431bf802a80fa097f1e5f8e624 @NotNull diff --git a/src/main/java/org/bukkit/Server.java b/src/main/java/org/bukkit/Server.java -index 1092ceef77bad421df647d349d997d02b2ba80a9..3a17ccd041ca64c8ab0cd3edf4ce6fc1c6f2d459 100644 +index ea4d93680066295de9fd447eda58b93014eac635..78cd452e804310eb8ed954833f2b7431ad9101b0 100644 --- a/src/main/java/org/bukkit/Server.java +++ b/src/main/java/org/bukkit/Server.java -@@ -2163,5 +2163,83 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi +@@ -2170,5 +2170,83 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi * @return the global region scheduler */ public @NotNull io.papermc.paper.threadedregions.scheduler.GlobalRegionScheduler getGlobalRegionScheduler(); diff --git a/patches/server/0004-Threaded-Regions.patch b/patches/server/0004-Threaded-Regions.patch index a589642..f28079f 100644 --- a/patches/server/0004-Threaded-Regions.patch +++ b/patches/server/0004-Threaded-Regions.patch @@ -3968,16 +3968,17 @@ index 0000000000000000000000000000000000000000..3549e5f3359f38b207e189d895954420 +} diff --git a/src/main/java/io/papermc/paper/threadedregions/RegionisedServer.java b/src/main/java/io/papermc/paper/threadedregions/RegionisedServer.java new file mode 100644 -index 0000000000000000000000000000000000000000..99c258c113cd4257e54f795e09d6d8f3aa2f4e33 +index 0000000000000000000000000000000000000000..796421515e3c41e3ff86c5fda844771909114f36 --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/RegionisedServer.java -@@ -0,0 +1,370 @@ +@@ -0,0 +1,375 @@ +package io.papermc.paper.threadedregions; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool; +import com.mojang.authlib.GameProfile; +import com.mojang.logging.LogUtils; ++import io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler; +import io.papermc.paper.util.TickThread; +import net.minecraft.CrashReport; +import net.minecraft.ReportedException; @@ -3998,6 +3999,7 @@ index 0000000000000000000000000000000000000000..99c258c113cd4257e54f795e09d6d8f3 +import net.minecraft.util.Mth; +import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.levelgen.LegacyRandomSource; ++import org.bukkit.Bukkit; +import org.slf4j.Logger; +import java.util.ArrayList; +import java.util.Arrays; @@ -4166,6 +4168,9 @@ index 0000000000000000000000000000000000000000..99c258c113cd4257e54f795e09d6d8f3 + + private void globalTick(final int tickCount) { + ++this.tickCount; ++ // scheduler ++ ((FoliaGlobalRegionScheduler)Bukkit.getGlobalRegionScheduler()).tick(); ++ + // commands + ((DedicatedServer)MinecraftServer.getServer()).handleConsoleInputs(); + @@ -4344,17 +4349,16 @@ index 0000000000000000000000000000000000000000..99c258c113cd4257e54f795e09d6d8f3 +} diff --git a/src/main/java/io/papermc/paper/threadedregions/RegionisedTaskQueue.java b/src/main/java/io/papermc/paper/threadedregions/RegionisedTaskQueue.java new file mode 100644 -index 0000000000000000000000000000000000000000..a40b88fbee7b92de38b49c4dfa7c279665043a80 +index 0000000000000000000000000000000000000000..b9c9c2db35924bcc5e752d39fa2949bd25aa54be --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/RegionisedTaskQueue.java -@@ -0,0 +1,754 @@ +@@ -0,0 +1,752 @@ +package io.papermc.paper.threadedregions; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; +import ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; -+import io.papermc.paper.chunk.system.io.RegionFileIOThread; +import io.papermc.paper.chunk.system.scheduling.ChunkHolderManager; +import io.papermc.paper.util.CoordinateUtils; +import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; @@ -4366,7 +4370,6 @@ index 0000000000000000000000000000000000000000..a40b88fbee7b92de38b49c4dfa7c2796 +import java.lang.invoke.VarHandle; +import java.util.ArrayDeque; +import java.util.Iterator; -+import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantLock; + @@ -8877,17 +8880,355 @@ index 0000000000000000000000000000000000000000..d016294fc7eafbddf6d2a758e5803498 + + private CommandUtil() {} +} -diff --git a/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaEntityScheduler.java b/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaEntityScheduler.java +diff --git a/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaAsyncScheduler.java b/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaAsyncScheduler.java new file mode 100644 -index 0000000000000000000000000000000000000000..c0b9f43fc53a5c9b3d9788f080e4fd3f4f94ebfe +index 0000000000000000000000000000000000000000..4874ca2ddf6b7bf7b818f97cfbc59d349a69f5ce --- /dev/null -+++ b/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaEntityScheduler.java -@@ -0,0 +1,38 @@ ++++ b/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaAsyncScheduler.java +@@ -0,0 +1,327 @@ +package io.papermc.paper.threadedregions.scheduler; + +import ca.spottedleaf.concurrentutil.util.Validate; -+import org.bukkit.craftbukkit.entity.CraftEntity; ++import com.mojang.logging.LogUtils; ++import org.bukkit.plugin.IllegalPluginAccessException; +import org.bukkit.plugin.Plugin; ++import org.slf4j.Logger; ++import java.util.Set; ++import java.util.concurrent.ConcurrentHashMap; ++import java.util.concurrent.Executor; ++import java.util.concurrent.Executors; ++import java.util.concurrent.ScheduledExecutorService; ++import java.util.concurrent.ScheduledFuture; ++import java.util.concurrent.SynchronousQueue; ++import java.util.concurrent.ThreadFactory; ++import java.util.concurrent.ThreadPoolExecutor; ++import java.util.concurrent.TimeUnit; ++import java.util.concurrent.atomic.AtomicInteger; ++import java.util.function.Consumer; ++import java.util.logging.Level; ++ ++public final class FoliaAsyncScheduler implements AsyncScheduler { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ private final Executor executors = new ThreadPoolExecutor(Math.max(4, Runtime.getRuntime().availableProcessors() / 2), Integer.MAX_VALUE, ++ 30L, TimeUnit.SECONDS, new SynchronousQueue<>(), ++ new ThreadFactory() { ++ private final AtomicInteger idGenerator = new AtomicInteger(); ++ ++ @Override ++ public Thread newThread(final Runnable run) { ++ final Thread ret = new Thread(run); ++ ++ ret.setName("Folia Async Scheduler Thread #" + this.idGenerator.getAndIncrement()); ++ ret.setPriority(Thread.NORM_PRIORITY - 1); ++ ret.setUncaughtExceptionHandler((final Thread thread, final Throwable thr) -> { ++ LOGGER.error("Uncaught exception in thread: " + thread.getName(), thr); ++ }); ++ ++ return ret; ++ } ++ } ++ ); ++ ++ private final ScheduledExecutorService timerThread = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() { ++ @Override ++ public Thread newThread(final Runnable run) { ++ final Thread ret = new Thread(run); ++ ++ ret.setName("Folia Async Scheduler Thread Timer"); ++ ret.setPriority(Thread.NORM_PRIORITY + 1); ++ ret.setUncaughtExceptionHandler((final Thread thread, final Throwable thr) -> { ++ LOGGER.error("Uncaught exception in thread: " + thread.getName(), thr); ++ }); ++ ++ return ret; ++ } ++ }); ++ ++ private final Set tasks = ConcurrentHashMap.newKeySet(); ++ ++ @Override ++ public ScheduledTask runNow(final Plugin plugin, final Consumer task) { ++ Validate.notNull(plugin, "Plugin may not be null"); ++ Validate.notNull(task, "Task may not be null"); ++ ++ if (!plugin.isEnabled()) { ++ throw new IllegalPluginAccessException("Plugin attempted to register task while disabled"); ++ } ++ ++ final AsyncScheduledTask ret = new AsyncScheduledTask(plugin, -1L, task, null, -1L); ++ ++ this.tasks.add(ret); ++ this.executors.execute(ret); ++ ++ if (!plugin.isEnabled()) { ++ // handle race condition where plugin is disabled asynchronously ++ ret.cancel(); ++ } ++ ++ return ret; ++ } ++ ++ @Override ++ public ScheduledTask runDelayed(final Plugin plugin, final Consumer task, final long delay, ++ final TimeUnit unit) { ++ Validate.notNull(plugin, "Plugin may not be null"); ++ Validate.notNull(task, "Task may not be null"); ++ Validate.notNull(unit, "Time unit may not be null"); ++ if (delay < 0L) { ++ throw new IllegalArgumentException("Delay may not be < 0"); ++ } ++ ++ if (!plugin.isEnabled()) { ++ throw new IllegalPluginAccessException("Plugin attempted to register task while disabled"); ++ } ++ ++ return this.scheduleTimerTask(plugin, task, delay, -1L, unit); ++ } ++ ++ @Override ++ public ScheduledTask runAtFixedRate(final Plugin plugin, final Consumer task, final long initialDelay, ++ final long period, final TimeUnit unit) { ++ Validate.notNull(plugin, "Plugin may not be null"); ++ Validate.notNull(task, "Task may not be null"); ++ Validate.notNull(unit, "Time unit may not be null"); ++ if (initialDelay < 0L) { ++ throw new IllegalArgumentException("Initial delay may not be < 0"); ++ } ++ if (period <= 0L) { ++ throw new IllegalArgumentException("Period may not be <= 0"); ++ } ++ ++ if (!plugin.isEnabled()) { ++ throw new IllegalPluginAccessException("Plugin attempted to register task while disabled"); ++ } ++ ++ return this.scheduleTimerTask(plugin, task, initialDelay, period, unit); ++ } ++ ++ private AsyncScheduledTask scheduleTimerTask(final Plugin plugin, final Consumer task, final long initialDelay, ++ final long period, final TimeUnit unit) { ++ final AsyncScheduledTask ret = new AsyncScheduledTask( ++ plugin, period <= 0 ? period : unit.toNanos(period), task, null, ++ System.nanoTime() + unit.toNanos(initialDelay) ++ ); ++ ++ synchronized (ret) { ++ // even though ret is not published, we need to synchronise while scheduling to avoid a race condition ++ // for when a scheduled task immediately executes before we update the delay field and state field ++ ret.setDelay(this.timerThread.schedule(ret, initialDelay, unit)); ++ this.tasks.add(ret); ++ } ++ ++ if (!plugin.isEnabled()) { ++ // handle race condition where plugin is disabled asynchronously ++ ret.cancel(); ++ } ++ ++ return ret; ++ } ++ ++ @Override ++ public void cancelTasks(final Plugin plugin) { ++ Validate.notNull(plugin, "Plugin may not be null"); ++ ++ for (final AsyncScheduledTask task : this.tasks) { ++ if (task.plugin == plugin) { ++ task.cancel(); ++ } ++ } ++ } ++ ++ private final class AsyncScheduledTask implements ScheduledTask, Runnable { ++ ++ private static final int STATE_ON_TIMER = 0; ++ private static final int STATE_SCHEDULED_EXECUTOR = 1; ++ private static final int STATE_EXECUTING = 2; ++ private static final int STATE_EXECUTING_CANCELLED = 3; ++ private static final int STATE_FINISHED = 4; ++ private static final int STATE_CANCELLED = 5; ++ ++ private final Plugin plugin; ++ private final long repeatDelay; // in ns ++ private Consumer run; ++ private ScheduledFuture delay; ++ private int state; ++ private long scheduleTarget; ++ ++ public AsyncScheduledTask(final Plugin plugin, final long repeatDelay, final Consumer run, ++ final ScheduledFuture delay, final long firstTarget) { ++ this.plugin = plugin; ++ this.repeatDelay = repeatDelay; ++ this.run = run; ++ this.delay = delay; ++ this.state = delay == null ? STATE_SCHEDULED_EXECUTOR : STATE_ON_TIMER; ++ this.scheduleTarget = firstTarget; ++ } ++ ++ private void setDelay(final ScheduledFuture delay) { ++ this.delay = delay; ++ this.state = STATE_SCHEDULED_EXECUTOR; ++ } ++ ++ @Override ++ public void run() { ++ final boolean repeating = this.isRepeatingTask(); ++ // try to advance state ++ final boolean timer; ++ synchronized (this) { ++ if (this.state == STATE_ON_TIMER) { ++ timer = true; ++ this.delay = null; ++ this.state = STATE_SCHEDULED_EXECUTOR; ++ } else if (this.state != STATE_SCHEDULED_EXECUTOR) { ++ // cancelled ++ if (this.state != STATE_CANCELLED) { ++ throw new IllegalStateException("Wrong state: " + this.state); ++ } ++ return; ++ } else { ++ timer = false; ++ this.state = STATE_EXECUTING; ++ } ++ } ++ ++ if (timer) { ++ // the scheduled executor is single thread, and unfortunately not expandable with threads ++ // so we just schedule onto the executor ++ FoliaAsyncScheduler.this.executors.execute(this); ++ return; ++ } ++ ++ try { ++ this.run.accept(this); ++ } catch (final Throwable throwable) { ++ this.plugin.getLogger().log(Level.WARNING, "Async task for " + this.plugin.getDescription().getFullName() + " generated an exception", throwable); ++ } finally { ++ boolean removeFromTasks = false; ++ synchronized (this) { ++ if (!repeating) { ++ // only want to execute once, so we're done ++ removeFromTasks = true; ++ this.state = STATE_FINISHED; ++ } else if (this.state != STATE_EXECUTING_CANCELLED) { ++ this.state = STATE_ON_TIMER; ++ // account for any delays, whether it be by task exec. or scheduler issues so that we keep ++ // the fixed schedule ++ final long currTime = System.nanoTime(); ++ final long delay = Math.max(0L, this.scheduleTarget + this.repeatDelay - currTime); ++ this.scheduleTarget = currTime + delay; ++ this.delay = FoliaAsyncScheduler.this.timerThread.schedule(this, delay, TimeUnit.NANOSECONDS); ++ } else { ++ // cancelled repeating task ++ removeFromTasks = true; ++ } ++ } ++ ++ if (removeFromTasks) { ++ this.run = null; ++ FoliaAsyncScheduler.this.tasks.remove(this); ++ } ++ } ++ } ++ ++ @Override ++ public Plugin getOwningPlugin() { ++ return this.plugin; ++ } ++ ++ @Override ++ public boolean isRepeatingTask() { ++ return this.repeatDelay > 0L; ++ } ++ ++ @Override ++ public CancelledState cancel() { ++ ScheduledFuture delay = null; ++ CancelledState ret; ++ synchronized (this) { ++ switch (this.state) { ++ case STATE_ON_TIMER: { ++ delay = this.delay; ++ this.delay = null; ++ this.state = STATE_CANCELLED; ++ ret = CancelledState.CANCELLED_BY_CALLER; ++ break; ++ } ++ case STATE_SCHEDULED_EXECUTOR: { ++ this.state = STATE_CANCELLED; ++ ret = CancelledState.CANCELLED_BY_CALLER; ++ break; ++ } ++ case STATE_EXECUTING: { ++ if (!this.isRepeatingTask()) { ++ return CancelledState.RUNNING; ++ } ++ this.state = STATE_EXECUTING_CANCELLED; ++ return CancelledState.NEXT_RUNS_CANCELLED; ++ } ++ case STATE_EXECUTING_CANCELLED: { ++ return CancelledState.NEXT_RUNS_CANCELLED_ALREADY; ++ } ++ case STATE_FINISHED: { ++ return CancelledState.ALREADY_EXECUTED; ++ } ++ case STATE_CANCELLED: { ++ return CancelledState.CANCELLED_ALREADY; ++ } ++ default: { ++ throw new IllegalStateException("Unknown state: " + this.state); ++ } ++ } ++ } ++ ++ if (delay != null) { ++ delay.cancel(false); ++ } ++ this.run = null; ++ FoliaAsyncScheduler.this.tasks.remove(this); ++ return ret; ++ } ++ ++ @Override ++ public ExecutionState getExecutionState() { ++ synchronized (this) { ++ switch (this.state) { ++ case STATE_ON_TIMER: ++ case STATE_SCHEDULED_EXECUTOR: ++ return ExecutionState.IDLE; ++ case STATE_EXECUTING: ++ return ExecutionState.RUNNING; ++ case STATE_EXECUTING_CANCELLED: ++ return ExecutionState.CANCELLED_RUNNING; ++ case STATE_FINISHED: ++ return ExecutionState.FINISHED; ++ case STATE_CANCELLED: ++ return ExecutionState.CANCELLED; ++ default: { ++ throw new IllegalStateException("Unknown state: " + this.state); ++ } ++ } ++ } ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaEntityScheduler.java b/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaEntityScheduler.java +new file mode 100644 +index 0000000000000000000000000000000000000000..60862f43a4469bccf3cf1b565de2956ba50396f6 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaEntityScheduler.java +@@ -0,0 +1,267 @@ ++package io.papermc.paper.threadedregions.scheduler; ++ ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.concurrentutil.util.Validate; ++import net.minecraft.world.entity.Entity; ++import org.bukkit.craftbukkit.entity.CraftEntity; ++import org.bukkit.plugin.IllegalPluginAccessException; ++import org.bukkit.plugin.Plugin; ++import org.jetbrains.annotations.Nullable; ++import java.lang.invoke.VarHandle; +import java.util.function.Consumer; +import java.util.logging.Level; + @@ -8904,6 +9245,10 @@ index 0000000000000000000000000000000000000000..c0b9f43fc53a5c9b3d9788f080e4fd3f + Validate.notNull(runnable, "Runnable may not be null"); + + return (final net.minecraft.world.entity.Entity nmsEntity) -> { ++ if (!plugin.isEnabled()) { ++ // don't execute if the plugin is disabled ++ return; ++ } + try { + runnable.run(); + } catch (final Throwable throwable) { @@ -8920,30 +9265,270 @@ index 0000000000000000000000000000000000000000..c0b9f43fc53a5c9b3d9788f080e4fd3f + + return this.entity.taskScheduler.schedule(runNMS, runRetired, delay); + } ++ ++ @Override ++ public @Nullable ScheduledTask run(final Plugin plugin, final Consumer task, final Runnable retired) { ++ return this.runDelayed(plugin, task, retired, 1); ++ } ++ ++ @Override ++ public @Nullable ScheduledTask runDelayed(final Plugin plugin, final Consumer task, final Runnable retired, ++ final int delayTicks) { ++ Validate.notNull(plugin, "Plugin may not be null"); ++ Validate.notNull(task, "Task may not be null"); ++ if (delayTicks <= 0) { ++ throw new IllegalArgumentException("Delay ticks may not be <= 0"); ++ } ++ ++ if (!plugin.isEnabled()) { ++ throw new IllegalPluginAccessException("Plugin attempted to register task while disabled"); ++ } ++ ++ final EntityScheduledTask ret = new EntityScheduledTask(plugin, -1, task, retired); ++ ++ if (!this.scheduleInternal(ret, delayTicks)) { ++ return null; ++ } ++ ++ if (!plugin.isEnabled()) { ++ // handle race condition where plugin is disabled asynchronously ++ ret.cancel(); ++ } ++ ++ return ret; ++ } ++ ++ @Override ++ public @Nullable ScheduledTask runAtFixedRate(final Plugin plugin, final Consumer task, ++ final Runnable retired, final int initialDelayTicks, final int periodTicks) { ++ Validate.notNull(plugin, "Plugin may not be null"); ++ Validate.notNull(task, "Task may not be null"); ++ if (initialDelayTicks <= 0) { ++ throw new IllegalArgumentException("Initial delay ticks may not be <= 0"); ++ } ++ if (periodTicks <= 0) { ++ throw new IllegalArgumentException("Period ticks may not be <= 0"); ++ } ++ ++ if (!plugin.isEnabled()) { ++ throw new IllegalPluginAccessException("Plugin attempted to register task while disabled"); ++ } ++ ++ final EntityScheduledTask ret = new EntityScheduledTask(plugin, periodTicks, task, retired); ++ ++ if (!this.scheduleInternal(ret, initialDelayTicks)) { ++ return null; ++ } ++ ++ if (!plugin.isEnabled()) { ++ // handle race condition where plugin is disabled asynchronously ++ ret.cancel(); ++ } ++ ++ return ret; ++ } ++ ++ private boolean scheduleInternal(final EntityScheduledTask ret, final int delay) { ++ return this.entity.taskScheduler.schedule(ret, ret, delay); ++ } ++ ++ private final class EntityScheduledTask implements ScheduledTask, Consumer { ++ ++ private static final int STATE_IDLE = 0; ++ private static final int STATE_EXECUTING = 1; ++ private static final int STATE_EXECUTING_CANCELLED = 2; ++ private static final int STATE_FINISHED = 3; ++ private static final int STATE_CANCELLED = 4; ++ ++ private final Plugin plugin; ++ private final int repeatDelay; // in ticks ++ private Consumer run; ++ private Runnable retired; ++ private volatile int state; ++ ++ private static final VarHandle STATE_HANDLE = ConcurrentUtil.getVarHandle(FoliaEntityScheduler.EntityScheduledTask.class, "state", int.class); ++ ++ private EntityScheduledTask(final Plugin plugin, final int repeatDelay, final Consumer run, final Runnable retired) { ++ this.plugin = plugin; ++ this.repeatDelay = repeatDelay; ++ this.run = run; ++ this.retired = retired; ++ } ++ ++ private final int getStateVolatile() { ++ return (int)STATE_HANDLE.get(this); ++ } ++ ++ private final int compareAndExchangeStateVolatile(final int expect, final int update) { ++ return (int)STATE_HANDLE.compareAndExchange(this, expect, update); ++ } ++ ++ private final void setStateVolatile(final int value) { ++ STATE_HANDLE.setVolatile(this, value); ++ } ++ ++ @Override ++ public void accept(final Entity entity) { ++ if (!this.plugin.isEnabled()) { ++ // don't execute if the plugin is disabled ++ this.setStateVolatile(STATE_CANCELLED); ++ return; ++ } ++ ++ final boolean repeating = this.isRepeatingTask(); ++ if (STATE_IDLE != this.compareAndExchangeStateVolatile(STATE_IDLE, STATE_EXECUTING)) { ++ // cancelled ++ return; ++ } ++ ++ final boolean retired = entity.isRemoved(); ++ ++ try { ++ if (!retired) { ++ this.run.accept(this); ++ } else { ++ if (this.retired != null) { ++ this.retired.run(); ++ } ++ } ++ } catch (final Throwable throwable) { ++ this.plugin.getLogger().log(Level.WARNING, "Entity task for " + this.plugin.getDescription().getFullName() + " generated an exception", throwable); ++ } finally { ++ boolean reschedule = false; ++ if (!repeating && !retired) { ++ this.setStateVolatile(STATE_FINISHED); ++ } else if (retired || !this.plugin.isEnabled()) { ++ this.setStateVolatile(STATE_CANCELLED); ++ } else if (STATE_EXECUTING == this.compareAndExchangeStateVolatile(STATE_EXECUTING, STATE_IDLE)) { ++ reschedule = true; ++ } // else: cancelled repeating task ++ ++ if (!reschedule) { ++ this.run = null; ++ this.retired = null; ++ } else { ++ if (!FoliaEntityScheduler.this.scheduleInternal(this, this.repeatDelay)) { ++ // the task itself must have removed the entity, so in this case we need to mark as cancelled ++ this.setStateVolatile(STATE_CANCELLED); ++ } ++ } ++ } ++ } ++ ++ @Override ++ public Plugin getOwningPlugin() { ++ return this.plugin; ++ } ++ ++ @Override ++ public boolean isRepeatingTask() { ++ return this.repeatDelay > 0; ++ } ++ ++ @Override ++ public CancelledState cancel() { ++ for (int curr = this.getStateVolatile();;) { ++ switch (curr) { ++ case STATE_IDLE: { ++ if (STATE_IDLE == (curr = this.compareAndExchangeStateVolatile(STATE_IDLE, STATE_CANCELLED))) { ++ this.state = STATE_CANCELLED; ++ this.run = null; ++ this.retired = null; ++ return CancelledState.CANCELLED_BY_CALLER; ++ } ++ // try again ++ continue; ++ } ++ case STATE_EXECUTING: { ++ if (!this.isRepeatingTask()) { ++ return CancelledState.RUNNING; ++ } ++ if (STATE_EXECUTING == (curr = this.compareAndExchangeStateVolatile(STATE_EXECUTING, STATE_EXECUTING_CANCELLED))) { ++ return CancelledState.NEXT_RUNS_CANCELLED; ++ } ++ // try again ++ continue; ++ } ++ case STATE_EXECUTING_CANCELLED: { ++ return CancelledState.NEXT_RUNS_CANCELLED_ALREADY; ++ } ++ case STATE_FINISHED: { ++ return CancelledState.ALREADY_EXECUTED; ++ } ++ case STATE_CANCELLED: { ++ return CancelledState.CANCELLED_ALREADY; ++ } ++ default: { ++ throw new IllegalStateException("Unknown state: " + curr); ++ } ++ } ++ } ++ } ++ ++ @Override ++ public ExecutionState getExecutionState() { ++ final int state = this.getStateVolatile(); ++ switch (state) { ++ case STATE_IDLE: ++ return ExecutionState.IDLE; ++ case STATE_EXECUTING: ++ return ExecutionState.RUNNING; ++ case STATE_EXECUTING_CANCELLED: ++ return ExecutionState.CANCELLED_RUNNING; ++ case STATE_FINISHED: ++ return ExecutionState.FINISHED; ++ case STATE_CANCELLED: ++ return ExecutionState.CANCELLED; ++ default: { ++ throw new IllegalStateException("Unknown state: " + state); ++ } ++ } ++ } ++ } +} diff --git a/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaGlobalRegionScheduler.java b/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaGlobalRegionScheduler.java new file mode 100644 -index 0000000000000000000000000000000000000000..cada55accf6c6b882918fe59f82cabe029f7b511 +index 0000000000000000000000000000000000000000..505329d601d56e42daa0b794092590cb16560f6d --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaGlobalRegionScheduler.java -@@ -0,0 +1,27 @@ +@@ -0,0 +1,266 @@ +package io.papermc.paper.threadedregions.scheduler; + ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.Validate; -+import io.papermc.paper.threadedregions.RegionisedServer; ++import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; ++import org.bukkit.plugin.IllegalPluginAccessException; +import org.bukkit.plugin.Plugin; ++import java.lang.invoke.VarHandle; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.function.Consumer; +import java.util.logging.Level; + +public final class FoliaGlobalRegionScheduler implements GlobalRegionScheduler { + -+ private static Runnable wrap(final Plugin plugin, final Runnable run) { -+ return () -> { -+ try { -+ run.run(); -+ } catch (final Throwable throwable) { -+ plugin.getLogger().log(Level.WARNING, "Global region task for " + plugin.getDescription().getFullName() + " generated an exception", throwable); ++ private long tickCount = 0L; ++ private final Object stateLock = new Object(); ++ private final Long2ObjectOpenHashMap> tasksByDeadline = new Long2ObjectOpenHashMap<>(); ++ ++ public void tick() { ++ final List run; ++ synchronized (this.stateLock) { ++ ++this.tickCount; ++ if (this.tasksByDeadline.isEmpty()) { ++ run = null; ++ } else { ++ run = this.tasksByDeadline.remove(this.tickCount); + } -+ }; ++ } ++ ++ if (run == null) { ++ return; ++ } ++ ++ for (int i = 0, len = run.size(); i < len; ++i) { ++ run.get(i).run(); ++ } + } + + @Override @@ -8951,23 +9536,262 @@ index 0000000000000000000000000000000000000000..cada55accf6c6b882918fe59f82cabe0 + Validate.notNull(plugin, "Plugin may not be null"); + Validate.notNull(run, "Runnable may not be null"); + -+ RegionisedServer.getInstance().addTaskWithoutNotify(wrap(plugin, run)); ++ this.run(plugin, (final ScheduledTask task) -> { ++ run.run(); ++ }); ++ } ++ ++ @Override ++ public ScheduledTask run(final Plugin plugin, final Consumer task) { ++ return this.runDelayed(plugin, task, 1); ++ } ++ ++ @Override ++ public ScheduledTask runDelayed(final Plugin plugin, final Consumer task, final int delayTicks) { ++ Validate.notNull(plugin, "Plugin may not be null"); ++ Validate.notNull(task, "Task may not be null"); ++ if (delayTicks <= 0) { ++ throw new IllegalArgumentException("Delay ticks may not be <= 0"); ++ } ++ ++ if (!plugin.isEnabled()) { ++ throw new IllegalPluginAccessException("Plugin attempted to register task while disabled"); ++ } ++ ++ final GlobalScheduledTask ret = new GlobalScheduledTask(plugin, -1, task); ++ ++ this.scheduleInternal(ret, delayTicks); ++ ++ if (!plugin.isEnabled()) { ++ // handle race condition where plugin is disabled asynchronously ++ ret.cancel(); ++ } ++ ++ return ret; ++ } ++ ++ @Override ++ public ScheduledTask runAtFixedRate(final Plugin plugin, final Consumer task, final int initialDelayTicks, final int periodTicks) { ++ Validate.notNull(plugin, "Plugin may not be null"); ++ Validate.notNull(task, "Task may not be null"); ++ if (initialDelayTicks <= 0) { ++ throw new IllegalArgumentException("Initial delay ticks may not be <= 0"); ++ } ++ if (periodTicks <= 0) { ++ throw new IllegalArgumentException("Period ticks may not be <= 0"); ++ } ++ ++ if (!plugin.isEnabled()) { ++ throw new IllegalPluginAccessException("Plugin attempted to register task while disabled"); ++ } ++ ++ final GlobalScheduledTask ret = new GlobalScheduledTask(plugin, periodTicks, task); ++ ++ this.scheduleInternal(ret, initialDelayTicks); ++ ++ if (!plugin.isEnabled()) { ++ // handle race condition where plugin is disabled asynchronously ++ ret.cancel(); ++ } ++ ++ return ret; ++ } ++ ++ @Override ++ public void cancelTasks(final Plugin plugin) { ++ Validate.notNull(plugin, "Plugin may not be null"); ++ ++ final List toCancel = new ArrayList<>(); ++ synchronized (this.stateLock) { ++ for (final List tasks : this.tasksByDeadline.values()) { ++ for (int i = 0, len = tasks.size(); i < len; ++i) { ++ final GlobalScheduledTask task = tasks.get(i); ++ if (task.plugin == plugin) { ++ toCancel.add(task); ++ } ++ } ++ } ++ } ++ ++ for (int i = 0, len = toCancel.size(); i < len; ++i) { ++ toCancel.get(i).cancel(); ++ } ++ } ++ ++ private void scheduleInternal(final GlobalScheduledTask task, final int delay) { ++ // note: delay > 0 ++ synchronized (this.stateLock) { ++ this.tasksByDeadline.computeIfAbsent(this.tickCount + delay, (final long keyInMap) -> { ++ return new ArrayList<>(); ++ }).add(task); ++ } ++ } ++ ++ private final class GlobalScheduledTask implements ScheduledTask, Runnable { ++ ++ private static final int STATE_IDLE = 0; ++ private static final int STATE_EXECUTING = 1; ++ private static final int STATE_EXECUTING_CANCELLED = 2; ++ private static final int STATE_FINISHED = 3; ++ private static final int STATE_CANCELLED = 4; ++ ++ private final Plugin plugin; ++ private final int repeatDelay; // in ticks ++ private Consumer run; ++ private volatile int state; ++ ++ private static final VarHandle STATE_HANDLE = ConcurrentUtil.getVarHandle(GlobalScheduledTask.class, "state", int.class); ++ ++ private GlobalScheduledTask(final Plugin plugin, final int repeatDelay, final Consumer run) { ++ this.plugin = plugin; ++ this.repeatDelay = repeatDelay; ++ this.run = run; ++ } ++ ++ private final int getStateVolatile() { ++ return (int)STATE_HANDLE.get(this); ++ } ++ ++ private final int compareAndExchangeStateVolatile(final int expect, final int update) { ++ return (int)STATE_HANDLE.compareAndExchange(this, expect, update); ++ } ++ ++ private final void setStateVolatile(final int value) { ++ STATE_HANDLE.setVolatile(this, value); ++ } ++ ++ @Override ++ public void run() { ++ final boolean repeating = this.isRepeatingTask(); ++ if (STATE_IDLE != this.compareAndExchangeStateVolatile(STATE_IDLE, STATE_EXECUTING)) { ++ // cancelled ++ return; ++ } ++ ++ try { ++ this.run.accept(this); ++ } catch (final Throwable throwable) { ++ this.plugin.getLogger().log(Level.WARNING, "Global task for " + this.plugin.getDescription().getFullName() + " generated an exception", throwable); ++ } finally { ++ boolean reschedule = false; ++ if (!repeating) { ++ this.setStateVolatile(STATE_FINISHED); ++ } else if (STATE_EXECUTING == this.compareAndExchangeStateVolatile(STATE_EXECUTING, STATE_IDLE)) { ++ reschedule = true; ++ } // else: cancelled repeating task ++ ++ if (!reschedule) { ++ this.run = null; ++ } else { ++ FoliaGlobalRegionScheduler.this.scheduleInternal(this, this.repeatDelay); ++ } ++ } ++ } ++ ++ @Override ++ public Plugin getOwningPlugin() { ++ return this.plugin; ++ } ++ ++ @Override ++ public boolean isRepeatingTask() { ++ return this.repeatDelay > 0; ++ } ++ ++ @Override ++ public CancelledState cancel() { ++ for (int curr = this.getStateVolatile();;) { ++ switch (curr) { ++ case STATE_IDLE: { ++ if (STATE_IDLE == (curr = this.compareAndExchangeStateVolatile(STATE_IDLE, STATE_CANCELLED))) { ++ this.state = STATE_CANCELLED; ++ this.run = null; ++ return CancelledState.CANCELLED_BY_CALLER; ++ } ++ // try again ++ continue; ++ } ++ case STATE_EXECUTING: { ++ if (!this.isRepeatingTask()) { ++ return CancelledState.RUNNING; ++ } ++ if (STATE_EXECUTING == (curr = this.compareAndExchangeStateVolatile(STATE_EXECUTING, STATE_EXECUTING_CANCELLED))) { ++ return CancelledState.NEXT_RUNS_CANCELLED; ++ } ++ // try again ++ continue; ++ } ++ case STATE_EXECUTING_CANCELLED: { ++ return CancelledState.NEXT_RUNS_CANCELLED_ALREADY; ++ } ++ case STATE_FINISHED: { ++ return CancelledState.ALREADY_EXECUTED; ++ } ++ case STATE_CANCELLED: { ++ return CancelledState.CANCELLED_ALREADY; ++ } ++ default: { ++ throw new IllegalStateException("Unknown state: " + curr); ++ } ++ } ++ } ++ } ++ ++ @Override ++ public ExecutionState getExecutionState() { ++ final int state = this.getStateVolatile(); ++ switch (state) { ++ case STATE_IDLE: ++ return ExecutionState.IDLE; ++ case STATE_EXECUTING: ++ return ExecutionState.RUNNING; ++ case STATE_EXECUTING_CANCELLED: ++ return ExecutionState.CANCELLED_RUNNING; ++ case STATE_FINISHED: ++ return ExecutionState.FINISHED; ++ case STATE_CANCELLED: ++ return ExecutionState.CANCELLED; ++ default: { ++ throw new IllegalStateException("Unknown state: " + state); ++ } ++ } ++ } + } +} diff --git a/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaRegionisedScheduler.java b/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaRegionisedScheduler.java new file mode 100644 -index 0000000000000000000000000000000000000000..e4637086ba0095341da9c2bfe20083375c82bc01 +index 0000000000000000000000000000000000000000..20bae52fc52659b5e1fb05ad8247da9524bb3c9d --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaRegionisedScheduler.java -@@ -0,0 +1,37 @@ +@@ -0,0 +1,430 @@ +package io.papermc.paper.threadedregions.scheduler; + ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.Validate; ++import io.papermc.paper.chunk.system.scheduling.ChunkHolderManager; ++import io.papermc.paper.threadedregions.RegionisedData; +import io.papermc.paper.threadedregions.RegionisedServer; ++import io.papermc.paper.threadedregions.TickRegionScheduler; ++import io.papermc.paper.threadedregions.TickRegions; ++import io.papermc.paper.util.CoordinateUtils; ++import it.unimi.dsi.fastutil.longs.Long2ObjectMap; ++import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; ++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; ++import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.TicketType; ++import net.minecraft.util.Unit; ++import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.craftbukkit.CraftWorld; ++import org.bukkit.plugin.IllegalPluginAccessException; +import org.bukkit.plugin.Plugin; ++import java.lang.invoke.VarHandle; ++import java.util.ArrayList; ++import java.util.Iterator; ++import java.util.List; ++import java.util.function.Consumer; +import java.util.logging.Level; + +public final class FoliaRegionisedScheduler implements RegionisedScheduler { @@ -8982,6 +9806,31 @@ index 0000000000000000000000000000000000000000..e4637086ba0095341da9c2bfe2008337 + }; + } + ++ private static final RegionisedData SCHEDULER_DATA = new RegionisedData<>(null, Scheduler::new, Scheduler.REGIONISER_CALLBACK); ++ ++ private static void scheduleInternalOnRegion(final LocationScheduledTask task, final int delay) { ++ SCHEDULER_DATA.get().queueTask(task, delay); ++ } ++ ++ private static void scheduleInternalOffRegion(final LocationScheduledTask task, final int delay) { ++ final Location location = task.location; ++ if (location == null) { ++ // cancelled ++ return; ++ } ++ ++ final World world = Validate.notNull(location.getWorld(), "Location world may not be null"); ++ ++ final int chunkX = location.getBlockX() >> 4; ++ final int chunkZ = location.getBlockZ() >> 4; ++ ++ RegionisedServer.getInstance().taskQueue.queueTickTaskQueue( ++ ((CraftWorld)world).getHandle(), chunkX, chunkZ, () -> { ++ scheduleInternalOnRegion(task, delay); ++ } ++ ); ++ } ++ + @Override + public void execute(final Plugin plugin, final Location location, final Runnable run) { + Validate.notNull(plugin, "Plugin may not be null"); @@ -8989,6 +9838,7 @@ index 0000000000000000000000000000000000000000..e4637086ba0095341da9c2bfe2008337 + Validate.notNull(run, "Runnable may not be null"); + + final World world = Validate.notNull(location.getWorld(), "Location world may not be null"); ++ + final int chunkX = location.getBlockX() >> 4; + final int chunkZ = location.getBlockZ() >> 4; + @@ -8996,6 +9846,353 @@ index 0000000000000000000000000000000000000000..e4637086ba0095341da9c2bfe2008337 + ((CraftWorld)world).getHandle(), chunkX, chunkZ, wrap(plugin, location.clone(), run) + ); + } ++ ++ @Override ++ public ScheduledTask run(final Plugin plugin, final Location location, final Consumer task) { ++ return this.runDelayed(plugin, location, task, 1); ++ } ++ ++ @Override ++ public ScheduledTask runDelayed(final Plugin plugin, Location location, final Consumer task, ++ final int delayTicks) { ++ Validate.notNull(plugin, "Plugin may not be null"); ++ Validate.notNull(location, "Location may not be null"); ++ Validate.notNull(task, "Task may not be null"); ++ if (delayTicks <= 0) { ++ throw new IllegalArgumentException("Delay ticks may not be <= 0"); ++ } ++ ++ if (!plugin.isEnabled()) { ++ throw new IllegalPluginAccessException("Plugin attempted to register task while disabled"); ++ } ++ ++ location = location.clone(); ++ ++ final LocationScheduledTask ret = new LocationScheduledTask(plugin, location, -1, task); ++ ++ if (Bukkit.isOwnedByCurrentRegion(location)) { ++ scheduleInternalOnRegion(ret, delayTicks); ++ } else { ++ scheduleInternalOffRegion(ret, delayTicks); ++ } ++ ++ if (!plugin.isEnabled()) { ++ // handle race condition where plugin is disabled asynchronously ++ ret.cancel(); ++ } ++ ++ return ret; ++ } ++ ++ @Override ++ public ScheduledTask runAtFixedRate(final Plugin plugin, Location location, final Consumer task, ++ final int initialDelayTicks, final int periodTicks) { ++ Validate.notNull(plugin, "Plugin may not be null"); ++ Validate.notNull(location, "Location may not be null"); ++ Validate.notNull(task, "Task may not be null"); ++ if (initialDelayTicks <= 0) { ++ throw new IllegalArgumentException("Initial delay ticks may not be <= 0"); ++ } ++ if (periodTicks <= 0) { ++ throw new IllegalArgumentException("Period ticks may not be <= 0"); ++ } ++ ++ if (!plugin.isEnabled()) { ++ throw new IllegalPluginAccessException("Plugin attempted to register task while disabled"); ++ } ++ ++ location = location.clone(); ++ ++ final LocationScheduledTask ret = new LocationScheduledTask(plugin, location, periodTicks, task); ++ ++ if (Bukkit.isOwnedByCurrentRegion(location)) { ++ scheduleInternalOnRegion(ret, initialDelayTicks); ++ } else { ++ scheduleInternalOffRegion(ret, initialDelayTicks); ++ } ++ ++ if (!plugin.isEnabled()) { ++ // handle race condition where plugin is disabled asynchronously ++ ret.cancel(); ++ } ++ ++ return ret; ++ } ++ ++ public void tick() { ++ SCHEDULER_DATA.get().tick(); ++ } ++ ++ private static final class Scheduler { ++ private static final RegionisedData.RegioniserCallback REGIONISER_CALLBACK = new RegionisedData.RegioniserCallback<>() { ++ @Override ++ public void merge(final Scheduler from, final Scheduler into, final long fromTickOffset) { ++ for (final Iterator>>> sectionIterator = from.tasksByDeadlineBySection.long2ObjectEntrySet().fastIterator(); ++ sectionIterator.hasNext();) { ++ final Long2ObjectMap.Entry>> entry = sectionIterator.next(); ++ final long sectionKey = entry.getLongKey(); ++ final Long2ObjectOpenHashMap> section = entry.getValue(); ++ ++ final Long2ObjectOpenHashMap> sectionAdjusted = new Long2ObjectOpenHashMap<>(section.size()); ++ ++ for (final Iterator>> iterator = section.long2ObjectEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Long2ObjectMap.Entry> e = iterator.next(); ++ final long newTick = e.getLongKey() + fromTickOffset; ++ final List tasks = e.getValue(); ++ ++ sectionAdjusted.put(newTick, tasks); ++ } ++ ++ into.tasksByDeadlineBySection.put(sectionKey, sectionAdjusted); ++ } ++ } ++ ++ @Override ++ public void split(final Scheduler from, final int chunkToRegionShift, final Long2ReferenceOpenHashMap regionToData, ++ final ReferenceOpenHashSet dataSet) { ++ for (final Scheduler into : dataSet) { ++ into.tickCount = from.tickCount; ++ } ++ ++ for (final Iterator>>> sectionIterator = from.tasksByDeadlineBySection.long2ObjectEntrySet().fastIterator(); ++ sectionIterator.hasNext();) { ++ final Long2ObjectMap.Entry>> entry = sectionIterator.next(); ++ final long sectionKey = entry.getLongKey(); ++ final Long2ObjectOpenHashMap> section = entry.getValue(); ++ ++ final Scheduler into = regionToData.get(sectionKey); ++ ++ into.tasksByDeadlineBySection.put(sectionKey, section); ++ } ++ } ++ }; ++ ++ private long tickCount = 0L; ++ // map of region section -> map of deadline -> list of tasks ++ private final Long2ObjectOpenHashMap>> tasksByDeadlineBySection = new Long2ObjectOpenHashMap<>(); ++ ++ private void addTicket(final int sectionX, final int sectionZ) { ++ final ServerLevel world = TickRegionScheduler.getCurrentRegionisedWorldData().world; ++ final int chunkX = sectionX << TickRegions.getRegionChunkShift(); ++ final int chunkZ = sectionZ << TickRegions.getRegionChunkShift(); ++ ++ world.chunkTaskScheduler.chunkHolderManager.addTicketAtLevel( ++ TicketType.REGION_SCHEDULER_API_HOLD, chunkX, chunkZ, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE ++ ); ++ } ++ ++ private void removeTicket(final long sectionKey) { ++ final ServerLevel world = TickRegionScheduler.getCurrentRegionisedWorldData().world; ++ final int chunkX = CoordinateUtils.getChunkX(sectionKey) << TickRegions.getRegionChunkShift(); ++ final int chunkZ = CoordinateUtils.getChunkZ(sectionKey) << TickRegions.getRegionChunkShift(); ++ ++ world.chunkTaskScheduler.chunkHolderManager.removeTicketAtLevel( ++ TicketType.REGION_SCHEDULER_API_HOLD, chunkX, chunkZ, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE ++ ); ++ } ++ ++ private void queueTask(final LocationScheduledTask task, final int delay) { ++ // note: must be on the thread that owns this scheduler ++ // note: delay > 0 ++ ++ final Location location = task.location; ++ if (location == null) { ++ // cancelled ++ return; ++ } ++ ++ final int sectionX = (location.getBlockX() >> 4) >> TickRegions.getRegionChunkShift(); ++ final int sectionZ = (location.getBlockZ() >> 4) >> TickRegions.getRegionChunkShift(); ++ ++ final Long2ObjectOpenHashMap> section = ++ this.tasksByDeadlineBySection.computeIfAbsent(CoordinateUtils.getChunkKey(sectionX, sectionZ), (final long keyInMap) -> { ++ return new Long2ObjectOpenHashMap<>(); ++ } ++ ); ++ ++ if (section.isEmpty()) { ++ // need to keep the scheduler loaded for this location in order for tick() to be called... ++ this.addTicket(sectionX, sectionZ); ++ } ++ ++ section.computeIfAbsent(this.tickCount + delay, (final long keyInMap) -> { ++ return new ArrayList<>(); ++ }).add(task); ++ } ++ ++ public void tick() { ++ ++this.tickCount; ++ ++ final List run = new ArrayList<>(); ++ ++ for (final Iterator>>> sectionIterator = this.tasksByDeadlineBySection.long2ObjectEntrySet().fastIterator(); ++ sectionIterator.hasNext();) { ++ final Long2ObjectMap.Entry>> entry = sectionIterator.next(); ++ final long sectionKey = entry.getLongKey(); ++ final Long2ObjectOpenHashMap> section = entry.getValue(); ++ ++ final List tasks = section.remove(this.tickCount); ++ ++ if (tasks == null) { ++ continue; ++ } ++ ++ run.addAll(tasks); ++ ++ if (section.isEmpty()) { ++ this.removeTicket(sectionKey); ++ sectionIterator.remove(); ++ } ++ } ++ ++ for (int i = 0, len = run.size(); i < len; ++i) { ++ run.get(i).run(); ++ } ++ } ++ } ++ ++ private static final class LocationScheduledTask implements ScheduledTask, Runnable { ++ ++ private static final int STATE_IDLE = 0; ++ private static final int STATE_EXECUTING = 1; ++ private static final int STATE_EXECUTING_CANCELLED = 2; ++ private static final int STATE_FINISHED = 3; ++ private static final int STATE_CANCELLED = 4; ++ ++ private final Plugin plugin; ++ private Location location; ++ private final int repeatDelay; // in ticks ++ private Consumer run; ++ ++ private volatile int state; ++ private static final VarHandle STATE_HANDLE = ConcurrentUtil.getVarHandle(LocationScheduledTask.class, "state", int.class); ++ ++ private LocationScheduledTask(final Plugin plugin, final Location location, final int repeatDelay, final Consumer run) { ++ this.plugin = plugin; ++ this.location = location; ++ this.repeatDelay = repeatDelay; ++ this.run = run; ++ } ++ ++ private final int getStateVolatile() { ++ return (int)STATE_HANDLE.get(this); ++ } ++ ++ private final int compareAndExchangeStateVolatile(final int expect, final int update) { ++ return (int)STATE_HANDLE.compareAndExchange(this, expect, update); ++ } ++ ++ private final void setStateVolatile(final int value) { ++ STATE_HANDLE.setVolatile(this, value); ++ } ++ ++ @Override ++ public void run() { ++ if (!this.plugin.isEnabled()) { ++ // don't execute if the plugin is disabled ++ return; ++ } ++ ++ final boolean repeating = this.isRepeatingTask(); ++ if (STATE_IDLE != this.compareAndExchangeStateVolatile(STATE_IDLE, STATE_EXECUTING)) { ++ // cancelled ++ return; ++ } ++ ++ try { ++ this.run.accept(this); ++ } catch (final Throwable throwable) { ++ this.plugin.getLogger().log(Level.WARNING, "Location task for " + this.plugin.getDescription().getFullName() + " at location " + this.location + " generated an exception", throwable); ++ } finally { ++ boolean reschedule = false; ++ if (!repeating) { ++ this.setStateVolatile(STATE_FINISHED); ++ } else if (!this.plugin.isEnabled()) { ++ this.setStateVolatile(STATE_CANCELLED); ++ } else if (STATE_EXECUTING == this.compareAndExchangeStateVolatile(STATE_EXECUTING, STATE_IDLE)) { ++ reschedule = true; ++ } // else: cancelled repeating task ++ ++ if (!reschedule) { ++ this.run = null; ++ this.location = null; ++ } else { ++ FoliaRegionisedScheduler.scheduleInternalOnRegion(this, this.repeatDelay); ++ } ++ } ++ } ++ ++ @Override ++ public Plugin getOwningPlugin() { ++ return this.plugin; ++ } ++ ++ @Override ++ public boolean isRepeatingTask() { ++ return this.repeatDelay > 0; ++ } ++ ++ @Override ++ public CancelledState cancel() { ++ for (int curr = this.getStateVolatile();;) { ++ switch (curr) { ++ case STATE_IDLE: { ++ if (STATE_IDLE == (curr = this.compareAndExchangeStateVolatile(STATE_IDLE, STATE_CANCELLED))) { ++ this.state = STATE_CANCELLED; ++ this.run = null; ++ this.location = null; ++ return CancelledState.CANCELLED_BY_CALLER; ++ } ++ // try again ++ continue; ++ } ++ case STATE_EXECUTING: { ++ if (!this.isRepeatingTask()) { ++ return CancelledState.RUNNING; ++ } ++ if (STATE_EXECUTING == (curr = this.compareAndExchangeStateVolatile(STATE_EXECUTING, STATE_EXECUTING_CANCELLED))) { ++ return CancelledState.NEXT_RUNS_CANCELLED; ++ } ++ // try again ++ continue; ++ } ++ case STATE_EXECUTING_CANCELLED: { ++ return CancelledState.NEXT_RUNS_CANCELLED_ALREADY; ++ } ++ case STATE_FINISHED: { ++ return CancelledState.ALREADY_EXECUTED; ++ } ++ case STATE_CANCELLED: { ++ return CancelledState.CANCELLED_ALREADY; ++ } ++ default: { ++ throw new IllegalStateException("Unknown state: " + curr); ++ } ++ } ++ } ++ } ++ ++ @Override ++ public ExecutionState getExecutionState() { ++ final int state = this.getStateVolatile(); ++ switch (state) { ++ case STATE_IDLE: ++ return ExecutionState.IDLE; ++ case STATE_EXECUTING: ++ return ExecutionState.RUNNING; ++ case STATE_EXECUTING_CANCELLED: ++ return ExecutionState.CANCELLED_RUNNING; ++ case STATE_FINISHED: ++ return ExecutionState.FINISHED; ++ case STATE_CANCELLED: ++ return ExecutionState.CANCELLED; ++ default: { ++ throw new IllegalStateException("Unknown state: " + state); ++ } ++ } ++ } ++ } +} diff --git a/src/main/java/io/papermc/paper/util/CachedLists.java b/src/main/java/io/papermc/paper/util/CachedLists.java index e08f4e39db4ee3fed62e37364d17dcc5c5683504..03d239460a2e856c1f59d6bcd95811c8e4e0cf6d 100644 @@ -9192,7 +10389,7 @@ index 6898c704e60d89d53c8ed114e5e12f73ed63605a..594ada3cdec25784c7bd6abb9ad42d3f * Converts an NMS entity's current location to a Bukkit Location * @param entity diff --git a/src/main/java/io/papermc/paper/util/TickThread.java b/src/main/java/io/papermc/paper/util/TickThread.java -index fc57850b80303fcade89ca95794f63910404a407..6182ba2a03ea8fd5acc527ea068560fecaa68763 100644 +index fc57850b80303fcade89ca95794f63910404a407..09ddf5394157f15850d22f298f5e55ab539ec77e 100644 --- a/src/main/java/io/papermc/paper/util/TickThread.java +++ b/src/main/java/io/papermc/paper/util/TickThread.java @@ -1,8 +1,21 @@ @@ -9238,7 +10435,7 @@ index fc57850b80303fcade89ca95794f63910404a407..6182ba2a03ea8fd5acc527ea068560fe public static void ensureTickThread(final ServerLevel world, final int chunkX, final int chunkZ, final String reason) { if (!isTickThreadFor(world, chunkX, chunkZ)) { MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); -@@ -77,11 +104,112 @@ public class TickThread extends Thread { +@@ -77,11 +104,114 @@ public class TickThread extends Thread { return Thread.currentThread() instanceof TickThread; } @@ -9292,15 +10489,17 @@ index fc57850b80303fcade89ca95794f63910404a407..6182ba2a03ea8fd5acc527ea068560fe + return isShutdownThread(); + } + -+ final int minSectionX = fromChunkX >> world.regioniser.sectionChunkShift; -+ final int maxSectionX = toChunkX >> world.regioniser.sectionChunkShift; -+ final int minSectionZ = fromChunkZ >> world.regioniser.sectionChunkShift; -+ final int maxSectionZ = toChunkZ >> world.regioniser.sectionChunkShift; ++ final int shift = world.regioniser.sectionChunkShift; ++ ++ final int minSectionX = fromChunkX >> shift; ++ final int maxSectionX = toChunkX >> shift; ++ final int minSectionZ = fromChunkZ >> shift; ++ final int maxSectionZ = toChunkZ >> shift; + + for (int secZ = minSectionZ; secZ <= maxSectionZ; ++secZ) { + for (int secX = minSectionX; secX <= maxSectionX; ++secX) { -+ final int lowerLeftCX = secX << world.regioniser.sectionChunkShift; -+ final int lowerLeftCZ = secZ << world.regioniser.sectionChunkShift; ++ final int lowerLeftCX = secX << shift; ++ final int lowerLeftCZ = secZ << shift; + if (world.regioniser.getRegionAtUnsynchronised(lowerLeftCX, lowerLeftCZ) != region) { + return false; + } @@ -10325,7 +11524,7 @@ index 27d4aa45e585842c04491839826d405d6f447f0e..e6ef0691588fbb33d47692db4269c565 // CraftBukkit start - SPIGOT-5477, MC-142590 } else if (MinecraftServer.getServer().hasStopped() || (listener instanceof ServerGamePacketListenerImpl && ((ServerGamePacketListenerImpl) listener).processedDisconnect)) { diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 2ee4e5e8d17a3a1e6a342c74b13135df030ffef6..4a4b19ebd3fe743ca957d5ab307ef4dc0a1becec 100644 +index 2ee4e5e8d17a3a1e6a342c74b13135df030ffef6..fb2f7cde13ef96334a42448798a4902e8c1e06a3 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -291,7 +291,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0) { this.playerList.saveAll(playerSaveInterval); } @@ -10679,7 +11879,7 @@ index 2ee4e5e8d17a3a1e6a342c74b13135df030ffef6..4a4b19ebd3fe743ca957d5ab307ef4dc } } } finally { -@@ -1441,16 +1585,17 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { return worldserver + " " + worldserver.dimension().location(); -@@ -1532,7 +1676,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { +@@ -35,6 +35,15 @@ public class TicketType { public static final TicketType POI_LOAD = create("poi_load", Long::compareTo); public static final TicketType UNLOAD_COOLDOWN = create("unload_cooldown", (u1, u2) -> 0, 5 * 20); // Paper end - rewrite chunk system @@ -15195,6 +16395,7 @@ index 97d1ff2af23bac14e67bca5896843325aaa5bfc1..77495a7bdde233c70a45e806446a59d6 + public static final TicketType NON_FULL_SYNC_LOAD = create("non_full_sync_load", Long::compareTo); + public static final TicketType NETHER_PORTAL_DOUBLE_CHECK = create("nether_portal_double_check", Long::compareTo); + public static final TicketType TELEPORT_HOLD_TICKET = create("teleport_hold_ticket", Long::compareTo); ++ public static final TicketType REGION_SCHEDULER_API_HOLD = create("region_scheduler_api_hold", (a, b) -> 0); + // Folia end - region threading public static TicketType create(String name, Comparator argumentComparator) { @@ -21721,15 +22922,16 @@ index 7f1ac2cb29eb84833c0895442d611dfa0504527e..c79cfebc65fd04994735dabcf5bb6e6c LevelChunkTicks levelChunkTicks = this.allContainers.get(l); if (levelChunkTicks == null) { diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -index 2ea3778ee1348e5d06b15a2c5dc5d9bd4136dbe3..38d508b7a198df39de6d8ddc1b2aa6b8284f2d1b 100644 +index 2ea3778ee1348e5d06b15a2c5dc5d9bd4136dbe3..34dd05b737ee02200c5973ad21a600b2e2434d07 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -@@ -307,6 +307,75 @@ public final class CraftServer implements Server { +@@ -307,6 +307,81 @@ public final class CraftServer implements Server { CraftItemFactory.instance(); } + // Folia start - region threading API + private final io.papermc.paper.threadedregions.scheduler.FoliaRegionisedScheduler regionisedScheduler = new io.papermc.paper.threadedregions.scheduler.FoliaRegionisedScheduler(); ++ private final io.papermc.paper.threadedregions.scheduler.FoliaAsyncScheduler asyncScheduler = new io.papermc.paper.threadedregions.scheduler.FoliaAsyncScheduler(); + private final io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler globalRegionScheduler = new io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler(); + + @Override @@ -21738,6 +22940,11 @@ index 2ea3778ee1348e5d06b15a2c5dc5d9bd4136dbe3..38d508b7a198df39de6d8ddc1b2aa6b8 + } + + @Override ++ public final io.papermc.paper.threadedregions.scheduler.AsyncScheduler getAsyncScheduler() { ++ return this.asyncScheduler; ++ } ++ ++ @Override + public final io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler getGlobalRegionScheduler() { + return this.globalRegionScheduler; + } @@ -21800,7 +23007,7 @@ index 2ea3778ee1348e5d06b15a2c5dc5d9bd4136dbe3..38d508b7a198df39de6d8ddc1b2aa6b8 public CraftServer(DedicatedServer console, PlayerList playerList) { this.console = console; this.playerList = (DedicatedPlayerList) playerList; -@@ -879,6 +948,9 @@ public final class CraftServer implements Server { +@@ -879,6 +954,9 @@ public final class CraftServer implements Server { // NOTE: Should only be called from DedicatedServer.ah() public boolean dispatchServerCommand(CommandSender sender, ConsoleInput serverCommand) { @@ -21810,7 +23017,7 @@ index 2ea3778ee1348e5d06b15a2c5dc5d9bd4136dbe3..38d508b7a198df39de6d8ddc1b2aa6b8 if (sender instanceof Conversable) { Conversable conversable = (Conversable) sender; -@@ -898,12 +970,44 @@ public final class CraftServer implements Server { +@@ -898,12 +976,44 @@ public final class CraftServer implements Server { } } @@ -21855,7 +23062,7 @@ index 2ea3778ee1348e5d06b15a2c5dc5d9bd4136dbe3..38d508b7a198df39de6d8ddc1b2aa6b8 // Paper Start if (!org.spigotmc.AsyncCatcher.shuttingDown && !Bukkit.isPrimaryThread()) { final CommandSender fSender = sender; -@@ -2913,7 +3017,7 @@ public final class CraftServer implements Server { +@@ -2913,7 +3023,7 @@ public final class CraftServer implements Server { @Override public int getCurrentTick() { diff --git a/patches/server/0009-Make-CraftEntity-getHandle-and-overrides-perform-thr.patch b/patches/server/0009-Make-CraftEntity-getHandle-and-overrides-perform-thr.patch index 69851df..083ba2f 100644 --- a/patches/server/0009-Make-CraftEntity-getHandle-and-overrides-perform-thr.patch +++ b/patches/server/0009-Make-CraftEntity-getHandle-and-overrides-perform-thr.patch @@ -86,6 +86,19 @@ index 58d39268a2608901a14696d36f3c59d8d6ac06e7..021177d19384ef898667b8a374cd3838 // Spigot end if (this.passengers.size() == 1 && this.passengers.get(0) == entity) { this.passengers = ImmutableList.of(); +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 34dd05b737ee02200c5973ad21a600b2e2434d07..306cc8fe9a90091dadac6a9f2d984342ff798218 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -378,7 +378,7 @@ public final class CraftServer implements Server { + + @Override + public final boolean isOwnedByCurrentRegion(Entity entity) { +- return io.papermc.paper.util.TickThread.isTickThreadFor(((org.bukkit.craftbukkit.entity.CraftEntity)entity).getHandle()); ++ return io.papermc.paper.util.TickThread.isTickThreadFor(((org.bukkit.craftbukkit.entity.CraftEntity)entity).getHandleRaw()); // Folia - add thread checks to getHandle + } + // Folia end - region threading API + diff --git a/src/main/java/org/bukkit/craftbukkit/entity/AbstractProjectile.java b/src/main/java/org/bukkit/craftbukkit/entity/AbstractProjectile.java index 825fdc6162797ade8e76e1ca3a863ed5fb48f936..f812ad4a4d6c640c3f3f2d5766ee2b5882583cc5 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/AbstractProjectile.java diff --git a/patches/server/0010-Disable-mid-tick-task-execution.patch b/patches/server/0010-Disable-mid-tick-task-execution.patch index c5d9ad5..d6de7d6 100644 --- a/patches/server/0010-Disable-mid-tick-task-execution.patch +++ b/patches/server/0010-Disable-mid-tick-task-execution.patch @@ -10,10 +10,10 @@ the impact from scaling the region threads, but is not a fix to the underlying issue. diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 4a4b19ebd3fe743ca957d5ab307ef4dc0a1becec..1e3acf15dc6c055212baa39905b9ce8fa33d081a 100644 +index fb2f7cde13ef96334a42448798a4902e8c1e06a3..d43202f7de86e8d0c74abc0b7ff23baa69da7c87 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -2872,6 +2872,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop +Date: Wed, 22 Mar 2023 14:40:24 -0700 +Subject: [PATCH] Throw UnsupportedOperationException() for broken APIs + + +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 306cc8fe9a90091dadac6a9f2d984342ff798218..e149cec9deb1bd34c5a687ffed7f698c7f980875 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -1266,6 +1266,7 @@ public final class CraftServer implements Server { + + @Override + public World createWorld(WorldCreator creator) { ++ if (true) throw new UnsupportedOperationException(); // Folia - not implemented properly yet + Preconditions.checkState(this.console.getAllLevels().iterator().hasNext(), "Cannot create additional worlds on STARTUP"); + //Preconditions.checkState(!this.console.isIteratingOverLevels, "Cannot create a world while worlds are being ticked"); // Paper - Cat - Temp disable. We'll see how this goes. + Validate.notNull(creator, "Creator may not be null"); +@@ -1406,6 +1407,7 @@ public final class CraftServer implements Server { + + @Override + public boolean unloadWorld(World world, boolean save) { ++ if (true) throw new UnsupportedOperationException(); // Folia - not implemented properly yet + //Preconditions.checkState(!this.console.isIteratingOverLevels, "Cannot unload a world while worlds are being ticked"); // Paper - Cat - Temp disable. We'll see how this goes. + if (world == null) { + return false; +diff --git a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboard.java b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboard.java +index fe57437155ff9471738d3b85e787350601b79584..4ccdcdc78c86a698d555b6dc53bbd8668ca41d39 100644 +--- a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboard.java ++++ b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboard.java +@@ -44,6 +44,7 @@ public final class CraftScoreboard implements org.bukkit.scoreboard.Scoreboard { + } + @Override + public CraftObjective registerNewObjective(String name, Criteria criteria, net.kyori.adventure.text.Component displayName, RenderType renderType) throws IllegalArgumentException { ++ if (true) throw new UnsupportedOperationException(); // Folia - not supported yet + if (displayName == null) { + displayName = net.kyori.adventure.text.Component.empty(); + } +@@ -212,6 +213,7 @@ public final class CraftScoreboard implements org.bukkit.scoreboard.Scoreboard { + + @Override + public Team registerNewTeam(String name) throws IllegalArgumentException { ++ if (true) throw new UnsupportedOperationException(); // Folia - not supported yet + Validate.notNull(name, "Team name cannot be null"); + Validate.isTrue(name.length() <= Short.MAX_VALUE, "Team name '" + name + "' is longer than the limit of 32767 characters"); + Validate.isTrue(this.board.getPlayerTeam(name) == null, "Team name '" + name + "' is already in use"); +@@ -239,6 +241,7 @@ public final class CraftScoreboard implements org.bukkit.scoreboard.Scoreboard { + + @Override + public void clearSlot(DisplaySlot slot) throws IllegalArgumentException { ++ if (true) throw new UnsupportedOperationException(); // Folia - not supported yet + Validate.notNull(slot, "Slot cannot be null"); + this.board.setDisplayObjective(CraftScoreboardTranslations.fromBukkitSlot(slot), null); + } +diff --git a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboardManager.java b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboardManager.java +index 138407c2d4b0bc55ddb9aac5d2aa3edadda090fb..071289fe33f444b903b61d6ec34c9ca4873c9ac5 100644 +--- a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboardManager.java ++++ b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboardManager.java +@@ -42,6 +42,7 @@ public final class CraftScoreboardManager implements ScoreboardManager { + + @Override + public CraftScoreboard getNewScoreboard() { ++ if (true) throw new UnsupportedOperationException(); // Folia - not supported yet + org.spigotmc.AsyncCatcher.catchOp("scoreboard creation"); // Spigot + CraftScoreboard scoreboard = new CraftScoreboard(new ServerScoreboard(this.server)); + // Paper start +@@ -68,6 +69,7 @@ public final class CraftScoreboardManager implements ScoreboardManager { + + // CraftBukkit method + public void setPlayerBoard(CraftPlayer player, org.bukkit.scoreboard.Scoreboard bukkitScoreboard) throws IllegalArgumentException { ++ if (true) throw new UnsupportedOperationException(); // Folia - not supported yet + Validate.isTrue(bukkitScoreboard instanceof CraftScoreboard, "Cannot set player scoreboard to an unregistered Scoreboard"); + + CraftScoreboard scoreboard = (CraftScoreboard) bukkitScoreboard;