Repeating `ScheduledTask` stays `CANCELLED_RUNNING` after its callback finishes
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 12.7k
- Forks
- 3.5k
- Avg merge
- 3d 13h
- Merged PRs (30d)
- 11
Description
Expected behavior
When a repeating ScheduledTask is cancelled while it is running, I would expect it to report CANCELLED_RUNNING until the callback finishes and CANCELLED afterwards.
Observed/Actual behavior
Future runs are cancelled correctly, but the task never leaves CANCELLED_RUNNING. Calling cancel() later also returns NEXT_RUNS_CANCELLED_ALREADY, even though the callback has already finished
I reproduced this with a repeating global-region task on Paper 26.2-83. The callback only ran once, but the state was still CANCELLED_RUNNING one and two seconds after it returned.
Log from the reproduction:
[21:36:47 INFO]: [ScheduledTaskStateRepro] callback-start run=1 state-before-cancel=RUNNING
[21:36:47 INFO]: [ScheduledTaskStateRepro] cancel-during-callback=NEXT_RUNS_CANCELLED
[21:36:47 INFO]: [ScheduledTaskStateRepro] state-after-cancel-in-callback=CANCELLED_RUNNING
[21:36:47 INFO]: [ScheduledTaskStateRepro] callback-returning run=1
[21:36:48 INFO]: [ScheduledTaskStateRepro] follow-up-after-callback callback-runs=1
[21:36:48 INFO]: [ScheduledTaskStateRepro] state-after-callback=CANCELLED_RUNNING
[21:36:48 INFO]: [ScheduledTaskStateRepro] cancel-after-callback=NEXT_RUNS_CANCELLED_ALREADY
[21:36:49 INFO]: [ScheduledTaskStateRepro] second-follow-up callback-runs=1 state=CANCELLED_RUNNING
Steps/models to reproduce
-
Install a plugin that does this:
private int callbackRuns; @Override public void onEnable() { var scheduler = this.getServer().getGlobalRegionScheduler(); var logger = this.getLogger(); scheduler.runAtFixedRate(this, task -> { int run = ++this.callbackRuns; logger.info("callback-start run=" + run + " state-before-cancel=" + task.getExecutionState()); logger.info("cancel-during-callback=" + task.cancel()); logger.info("state-after-cancel-in-callback=" + task.getExecutionState()); scheduler.runDelayed(this, _ -> { logger.info("follow-up-after-callback callback-runs=" + this.callbackRuns); logger.info("state-after-callback=" + task.getExecutionState()); logger.info("cancel-after-callback=" + task.cancel()); scheduler.runDelayed( this, _ -> logger.info("second-follow-up callback-runs=" + this.callbackRuns + " state=" + task.getExecutionState()), 20L ); }, 20L); logger.info("callback-returning run=" + run); }, 1L, 200L); } -
Start a fresh Paper server and wait two seconds after startup completes
-
Check the log after the delayed tasks run
Plugin and Datapack List
> plugins
[23:03:55 INFO]: ℹ Server Plugins (1):
[23:03:55 INFO]: Bukkit Plugins:
[23:03:55 INFO]: - ScheduledTaskStateRepro
> datapack list
[23:03:57 INFO]: There are 3 data pack(s) enabled: [vanilla (built-in)], [file/bukkit (world)], [paper (built-in)]
[23:03:57 INFO]: There are no more data packs available
Paper version
> version
[23:01:41 INFO]: Checking version, please wait...
[23:01:42 INFO]: This server is running Paper version 26.2-83-main@f9fb80b (2026-07-26T16:57:41Z) (Implementing API version 26.2.build.83-stable)
You are running the latest version
Other
I also checked the scheduler implementations, and the global, async, and entity schedulers all have the same missing state transition: they stop rescheduling when a running task is cancelled, but don't change the state from executing-cancelled to cancelled when the callback finishes
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by tracing the global, async, and entity scheduler implementations, especially the path taken when a running repeating task is cancelled and its callback returns. Reproduce the issue with the supplied plugin and verify that the task reports CANCELLED after the callback while future runs remain cancelled.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java
- Domain
- backend-api-design
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 65/100