Skip to content

Scheduler

Every component can inject a Scheduler. It is an ambient root like the plugin itself and needs no declaration. When the graph unloads, weftkit cancels every task scheduled through it.

Component-owned tasks

Inject TaskScope into a singleton for tasks tied to that component. Each consumer gets its own scope, cancelled before its unload, including failed construction or startup. Other components' tasks keep running. The bundled bukkit:scheduler module supplies it.

@Wired
@Singleton
final class HomesFile implements Loader {

    private final TaskScope tasks;

    HomesFile(JavaPlugin plugin, TaskScope tasks) {
        this.tasks = tasks;
        ...
    }

    @Override
    public boolean load() {
        ...
        tasks.global().runRepeating(this::save, 20L * 60 * 5, 20L * 60 * 5);
        return true;
    }

    @Override
    public void unload() {
        save();
    }
}

Import TaskScope from org.weftkit.wiring.bukkit.scheduler. The player-homes example uses this pattern.

TaskScope has the same scheduling API as Scheduler. Closing it rejects new work, including through retained region and async handles, and leaves the parent scheduler open. Already-running callbacks may finish; closing does not interrupt or wait for them.

Use scheduler.scope() for manual ownership and close it yourself, for example with try-with-resources. Only singletons can inject TaskScope; prototypes use manual ownership.

The API

The interface follows Folia's threaded regions. Every task names the Region it runs in or runs async(). On Bukkit and Paper every region is the main thread.

Region Bukkit and Paper Folia
global() Main thread Global region scheduler
at(Location) Main thread The region owning the Location
on(Entity) Main thread The Entity's own scheduler
async() Bukkit's async pool Folia's async scheduler

A Region offers:

  • run(task): runs on the next tick.
  • runLater(task, delayTicks): runs after the delay. A delay of 0 is the next tick.
  • runRepeating(task, delayTicks, periodTicks): runs every period after the delay.
  • isCurrentThread(): whether the current thread ticks this region.

async() offers the same three scheduling methods with Durations instead of ticks. Bukkit rounds a Duration up to whole ticks. Folia keeps millisecond precision.

scheduler.at(location).run(() -> world.spawnParticle(...));
scheduler.on(player).runLater(() -> player.sendMessage("..."), 20);
scheduler.async().run(this::saveToDisk);

Every scheduling method returns a Task handle with cancel() and isCancelled(). On Folia, a task whose entity was removed before it ran reports cancelled.

Check threads with isCurrentThread(), not Bukkit.isPrimaryThread(). Folia has no single main thread.

Lifecycle

On disable, weftkit tears down in this order:

  1. The modules deactivate.
  2. The singletons unload in reverse creation order, each closing its owned resources before its hook.
  3. The bundled bukkit:scheduler module, loaded first and so unloaded last, closes the Scheduler and cancels every task scheduled through it.

A module that starts a repeating task in activate cancels it in deactivate. The announcer example does this.

When startup aborts because a load hook failed, the same unload runs for the components that did load, whichever Scheduler the graph runs with. Tasks scheduled through the shared Scheduler remain queued until the scheduler module closes; component-owned tasks have already been cancelled before their owner's unload hook. Disable the module with @Registry(disable = BukkitModules.SCHEDULER) to cancel by hand.

cancelAll belongs to the adapter and only touches tasks scheduled through the Scheduler. Cancel your own tasks through their Task handles. A task scheduled directly with the platform scheduler stays the plugin's to cancel.

Teardown calls Scheduler.close(). Built-in schedulers permanently reject new work with IllegalStateException, including calls through retained Region and Async handles; use a fresh scheduler for a new enable. Pending tasks are cancelled, late platform handles are cancelled on arrival, and callbacks already running may finish. cancelAll() remains reusable. Custom schedulers should override close() to close admission; its compatibility default only calls cancelAll().

Folia

Any weftkit plugin runs on Folia after two changes. This applies to plugin.yml and paper-plugin.yml plugins alike.

  1. Add weftkit-folia to the dependencies that go into the jar.

    dependencies {
        implementation("org.weftkit:weftkit-folia:0.6.0")
    }
    
  2. Declare folia-supported: true in plugin.yml or paper-plugin.yml.

With weftkit-folia on the compile classpath, the Bukkit processor weaves the folia:scheduler module into the registry. The module's SchedulerDetection component provides the Scheduler. It picks Folia's regions on Folia and the Bukkit scheduler on every other server. On servers other than Folia, no Folia class is loaded.

weftkit-folia requires Folia 1.20.1 or newer.

Without weftkit-folia, a plugin that enables on Folia fails with this message:

Folia detected. Add weftkit-folia to the plugin's dependencies, or pass a Scheduler to BukkitWeft.enable

Three ways exist to override the detected scheduler:

  • A scheduler() override in the plugin main replaces it.
  • A scheduler passed to BukkitWeft.enable replaces it.
  • @Registry(disable = FoliaModules.SCHEDULER) drops the module.

BukkitWeft.scheduler(loader) returns the scheduler a loader runs with.

The Scheduler leaves out two parts of Folia's API. Use Paper's API directly for them:

  • A custom retired callback for entity tasks. weftkit's callback marks the Task cancelled.
  • Region tasks addressed by world and chunk coordinates instead of a Location.

The bundled metrics module also schedules through the Scheduler.

  • Lifecycle: where cancellation sits in startup and shutdown
  • Components: ambient roots
  • player-homes: an autosave task left to weftkit to cancel
  • announcer: a plugin.yml plugin that also runs on Folia
  • warps: a paper-plugin.yml plugin that does the same