Expected behavior
Bukkit.getAsyncScheduler().runDelayed(...) and Bukkit.getAsyncScheduler().runAtFixedRate(...) schedule the task using FoliaAsyncScheduler#timerThread: it doesn't matter if it's a delayed or a repeating task
FoliaAsyncScheduler.AsyncScheduledTask#run() gets run for the task
- All runs of the task are executed on
FoliaAsyncScheduler#executors
Observed/Actual behavior
Bukkit.getAsyncScheduler().runDelayed(...) and Bukkit.getAsyncScheduler().runAtFixedRate(...) schedule the task using FoliaAsyncScheduler#timerThread: it doesn't matter if it's a delayed or a repeating task
FoliaAsyncScheduler.AsyncScheduledTask#run() gets run for the task
- Each run is meant to be dispatched into the pool (
FoliaAsyncScheduler#executors) but the first one isn't. The work is dispatched into the pool only when AsyncScheduledTask#state is STATE_ON_TIMER, which is instead always STATE_SCHEDULED_EXECUTOR for first runs of all task:
AsyncScheduledTask gets initialized in FoliaAsyncScheduler#scheduleTimerTask(...) by its constructor: the delay argument is always null, so this.state = delay == null ? STATE_SCHEDULED_EXECUTOR : STATE_ON_TIMER becomes this.state = STATE_SCHEDULED_EXECUTOR
|
final AsyncScheduledTask ret = new AsyncScheduledTask( |
|
plugin, period <= 0 ? period : unit.toNanos(period), task, null, |
|
System.nanoTime() + unit.toNanos(initialDelay) |
|
); |
|
public AsyncScheduledTask(final Plugin plugin, final long repeatDelay, final Consumer<ScheduledTask> 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; |
|
} |
- Then,
AsyncScheduledTask#setDelay(ScheduledFuture<?>) gets called in the same #scheduleTimerTask(...) method, once again explicitly setting the state: this.state = STATE_SCHEDULED_EXECUTOR
|
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); |
|
private void setDelay(final ScheduledFuture<?> delay) { |
|
this.delay = delay; |
|
this.state = STATE_SCHEDULED_EXECUTOR; |
|
} |
- When the
AsyncScheduledTask#run() is called, the local timer variable responsible for dispatching the task into the pool or running it in-place is false, because of the state being STATE_SCHEDULED_EXECUTOR
|
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; |
|
} |
- Since
timer is false, the pool part is skipped and the task is run in-place
|
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); |
- If the task is a single-run delayed task, its state becomes
STATE_FINISHED, but if it's a repeating task its state finally becomes STATE_ON_TIMER, so going back to the 3rd point: during subsequent runs timer is true and the task is properly dispatched into the thread pool
Steps/models to reproduce
public void onEnable() {
Bukkit.getAsyncScheduler().runDelayed(this, task -> {
System.out.println("Starting task 1");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
System.out.println("Task 1 interrupted");
throw new RuntimeException(e);
}
System.out.println("Finishing task 1");
}, 5, TimeUnit.SECONDS);
Bukkit.getAsyncScheduler().runDelayed(this, task -> {
System.out.println("Starting task 2");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
System.out.println("Task 2 interrupted");
throw new RuntimeException(e);
}
System.out.println("Finishing task 2");
}, 7, TimeUnit.SECONDS);
}
Expected output:
...
[00:00:00] Done (...s)! For help, type "help"
[00:00:05] [TestPaperPlugin] [STDOUT] Starting task 1
[00:00:07] [TestPaperPlugin] [STDOUT] Starting task 2
[00:00:15] [TestPaperPlugin] [STDOUT] Finishing task 1
[00:00:17] [TestPaperPlugin] [STDOUT] Finishing task 2
...
Observed output:
...
[00:00:00] Done (...s)! For help, type "help"
[00:00:05] [TestPaperPlugin] [STDOUT] Starting task 1
[00:00:15] [TestPaperPlugin] [STDOUT] Finishing task 1
[00:00:15] [TestPaperPlugin] [STDOUT] Starting task 2
[00:00:25] [TestPaperPlugin] [STDOUT] Finishing task 2
...
Plugin and Datapack List
Only the test plugin
Paper version
> ver
[16:12:37 INFO]: Checking version, please wait...
[16:12:37 INFO]: This server is running Paper version 1.21.4-130-main@a392d47 (2025-01-28T00:39:56Z) (Implementing API version 1.21.4-R0.1-SNAPSHOT)
You are running the latest version
Other
sorry for way too many letters, hopefully that helps 🎄
Expected behavior
Bukkit.getAsyncScheduler().runDelayed(...)andBukkit.getAsyncScheduler().runAtFixedRate(...)schedule the task usingFoliaAsyncScheduler#timerThread: it doesn't matter if it's a delayed or a repeating taskFoliaAsyncScheduler.AsyncScheduledTask#run()gets run for the taskFoliaAsyncScheduler#executorsObserved/Actual behavior
Bukkit.getAsyncScheduler().runDelayed(...)andBukkit.getAsyncScheduler().runAtFixedRate(...)schedule the task usingFoliaAsyncScheduler#timerThread: it doesn't matter if it's a delayed or a repeating taskFoliaAsyncScheduler.AsyncScheduledTask#run()gets run for the taskFoliaAsyncScheduler#executors) but the first one isn't. The work is dispatched into the pool only whenAsyncScheduledTask#stateisSTATE_ON_TIMER, which is instead alwaysSTATE_SCHEDULED_EXECUTORfor first runs of all task:AsyncScheduledTaskgets initialized inFoliaAsyncScheduler#scheduleTimerTask(...)by its constructor: thedelayargument is alwaysnull, sothis.state = delay == null ? STATE_SCHEDULED_EXECUTOR : STATE_ON_TIMERbecomesthis.state = STATE_SCHEDULED_EXECUTORPaper/paper-server/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaAsyncScheduler.java
Lines 125 to 128 in a392d47
Paper/paper-server/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaAsyncScheduler.java
Lines 172 to 180 in a392d47
AsyncScheduledTask#setDelay(ScheduledFuture<?>)gets called in the same#scheduleTimerTask(...)method, once again explicitly setting the state:this.state = STATE_SCHEDULED_EXECUTORPaper/paper-server/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaAsyncScheduler.java
Lines 130 to 134 in a392d47
Paper/paper-server/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaAsyncScheduler.java
Lines 182 to 185 in a392d47
AsyncScheduledTask#run()is called, the localtimervariable responsible for dispatching the task into the pool or running it in-place isfalse, because of the state beingSTATE_SCHEDULED_EXECUTORPaper/paper-server/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaAsyncScheduler.java
Lines 193 to 206 in a392d47
timerisfalse, the pool part is skipped and the task is run in-placePaper/paper-server/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaAsyncScheduler.java
Lines 209 to 217 in a392d47
STATE_FINISHED, but if it's a repeating task its state finally becomesSTATE_ON_TIMER, so going back to the 3rd point: during subsequent runstimeristrueand the task is properly dispatched into the thread poolSteps/models to reproduce
Expected output:
Observed output:
Plugin and Datapack List
Only the test plugin
Paper version
Other
sorry for way too many letters, hopefully that helps 🎄