Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First executions of delayed and repeating tasks on FoliaAsyncScheduler run in a single timer thread, making tasks block each other #12038

Open
metabrixkt opened this issue Jan 28, 2025 · 0 comments
Labels
scope: api status: accepted Disputed bug is accepted as valid or Feature accepted as desired to be added. version: 1.21.4 Game version 1.21.4

Comments

@metabrixkt
Copy link
Contributor

metabrixkt commented Jan 28, 2025

Expected behavior

  1. 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
  2. FoliaAsyncScheduler.AsyncScheduledTask#run() gets run for the task
  3. All runs of the task are executed on FoliaAsyncScheduler#executors

Observed/Actual behavior

  1. 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
  2. FoliaAsyncScheduler.AsyncScheduledTask#run() gets run for the task
  3. 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:
    1. 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;
      }
    2. 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;
      }
    3. 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;
      }
    4. 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);
    5. 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 🎄

@papermc-sniffer papermc-sniffer bot added the version: 1.21.4 Game version 1.21.4 label Jan 28, 2025
@Malfrador Malfrador added status: accepted Disputed bug is accepted as valid or Feature accepted as desired to be added. scope: api and removed status: needs triage labels Feb 2, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
scope: api status: accepted Disputed bug is accepted as valid or Feature accepted as desired to be added. version: 1.21.4 Game version 1.21.4
Projects
None yet
Development

No branches or pull requests

2 participants