Skip to content

Scheduler

frosxt edited this page Apr 14, 2026 · 1 revision

Scheduler

com.github.frosxt.prisoncore.scheduler.api.TaskOrchestrator is the only sanctioned way for a module to schedule work. Don't call Bukkit.getScheduler() directly. The orchestrator is what lets the platform centralize thread management.

TaskOrchestrator

public interface TaskOrchestrator {
    TaskHandle mainThread(TaskSpec spec);
    TaskHandle io(TaskSpec spec);
    TaskHandle cpu(TaskSpec spec);
    CompletableFuture<Void> switchToMainThread();
}

Resolved through the service container:

final TaskOrchestrator orchestrator = context.services().resolve(TaskOrchestrator.class);

Picking the right pool

mainThread(TaskSpec) runs the task on the Bukkit main thread. Required for anything that touches entity state, world state, inventories, or fires Bukkit events. The implementation routes through Bukkit's scheduler under the hood.

io(TaskSpec) runs on a shared scheduled executor sized for I/O work. Use this for disk reads, database queries, network calls, and the periodic flush of any write-behind cache you maintain. The pool is sized by available CPU count and is daemon-threaded.

cpu(TaskSpec) runs on a separate scheduled executor sized for CPU-bound work. Use this when you need to do significant computation that shouldn't interfere with I/O latency. In practice most modules don't need to distinguish; defaulting to io is fine for almost everything.

switchToMainThread() returns a CompletableFuture<Void> that completes on the main thread. This is the right way to hop a continuation back from an async callback into main-thread code:

orchestrator.io(TaskSpec.builder(() -> {
    final var data = loadFromDatabase();
    orchestrator.switchToMainThread().thenRun(() -> player.sendMessage(data));
}).build());

TaskSpec

com.github.frosxt.prisoncore.scheduler.api.TaskSpec

The immutable description of a task. Build one with TaskSpec.builder(Runnable).

TaskSpec spec = TaskSpec.builder(this::doWork)
    .delay(Duration.ofMillis(500))
    .period(Duration.ofSeconds(30))
    .build();

Builder methods:

  • delay(Duration) — initial delay before the first execution. Defaults to zero.
  • period(Duration) — repeat interval. Omit this for a one-shot task.

A non-null period marks the spec as repeating. The same TaskSpec can be submitted to multiple pools, but typically you build it once where you submit it.

TaskHandle

com.github.frosxt.prisoncore.scheduler.api.TaskHandle

Returned by every scheduling call.

public interface TaskHandle {
    int id();
    void cancel();
    boolean isCancelled();
}

Hold onto the handle for any repeating task. Cancel it from your onDisable so the task stops running before the kernel shuts your services down.

public final class MyModule extends AbstractPlatformModule {

    private TaskHandle pollTask;

    @Override
    protected void onEnable(final ModuleContext context) {
        final TaskOrchestrator orchestrator = context.services().resolve(TaskOrchestrator.class);
        pollTask = orchestrator.io(TaskSpec.builder(this::poll)
            .delay(Duration.ofSeconds(5))
            .period(Duration.ofSeconds(60))
            .build());
    }

    @Override
    protected void onDisable(final ModuleContext context) {
        if (pollTask != null) {
            pollTask.cancel();
        }
    }

    private void poll() {
        // runs on an io thread every minute
    }
}

For one-shot tasks you can usually ignore the returned handle. For repeating tasks you almost always want it.

A word on threading correctness

The scheduler is the seam between platform-managed threads and Bukkit. Three rules to keep in mind:

  1. Anything that calls into Bukkit (entities, worlds, players, inventories, events) must run on the main thread. If you're not sure, route through mainThread or switchToMainThread.
  2. Anything that hits disk, a database, or the network must not run on the main thread. Use io.
  3. Continuations from CompletableFuture complete on whichever thread completed the future. If you wired up an io task that ends with .thenAccept(...), that callback runs on an io thread. Hop back to the main thread before touching Bukkit state.

If you find yourself wanting to call Thread.sleep(...) to delay something, you want a delay(...) on a TaskSpec instead.

Clone this wiki locally