Skip to content

Scheduler and Threading

Petrus Pradella edited this page Jul 23, 2026 · 1 revision

Scheduler and Threading

EverNifeCore gives you one platform-agnostic entry point - FCScheduler - for both off-thread work and hops onto the game's main thread. Off-thread work runs on a virtual-thread executor when the JVM supports it (Java 21+), and falls back to a bounded pool on older runtimes. The main-thread bridge is platform-specific: McFCScheduler on Bukkit, HyFCScheduler on Hytale.

FCScheduler (platform-agnostic)

br.com.finalcraft.evernifecore.scheduler.FCScheduler is a static utility. Its core is a single shared VirtualThreadedScheduledExecutor (from the EveryLibs executors package) named "fcscheduler".

import br.com.finalcraft.evernifecore.scheduler.FCScheduler;

// Run now, off the main thread.
FCScheduler.runAsync(() -> {
    // safe for blocking I/O: DB call, HTTP, file read, ...
});

// Run once after a delay (milliseconds), off the main thread.
FCScheduler.scheduleAsync(() -> reloadCaches(), 5_000);

// The underlying scheduled executor, for fixed-rate / fixed-delay scheduling.
FCScheduler.getScheduler().scheduleAtFixedRate(task, 0, 500, TimeUnit.MILLISECONDS);

Both runAsync and scheduleAsync wrap your Runnable so an uncaught Throwable is printed rather than killing the worker - a failing task never takes the pool down with it.

Virtual threads, with a fallback

The shared executor uses virtual threads on Java 21 or newer, which makes it ideal for tasks that block (Thread.sleep, socket/JDBC I/O): a blocked virtual thread parks cheaply instead of pinning an OS thread. On a JVM older than 21 it degrades gracefully to a fixed thread pool bounded to the number of CPU cores - the detection is done through reflection, so the same JAR runs everywhere. On a Bukkit 1.7.10 server (Java 8) you get the bounded pool; on a modern Java 21 host you get virtual threads, with no code change.

For an ad-hoc executor with the same policy, EveryLibs exposes FCExecutorsUtil.createVirtualExecutorIfPossible(name) (br.com.finalcraft.everylibs.executors.util), which returns an ExecutorService backed by virtual threads when possible and a plain pool otherwise. The legacy-import path uses it to fan work out across a temporary pool.

Getting back onto the main thread (Bukkit)

Most Bukkit API is only safe on the server's main thread. McFCScheduler (FCScheduler.getMinecraftScheduler(), or the static McFCScheduler.INSTANCE) is the bridge back.

McFCScheduler mc = FCScheduler.getMinecraftScheduler();

mc.runSync(() -> player.sendMessage("done"));       // next tick, on the main thread
mc.scheduleSyncInTicks(() -> spawnReward(), 20);     // 20 ticks (~1s) later, main thread
mc.scheduleSync(() -> flush(), 5_000);               // 5s later (ms delay), then hop to main
Method Behavior
runSync(Runnable) Runs the task on the main thread on the next tick (BukkitRunnable#runTask).
scheduleSyncInTicks(Runnable, long ticks) Runs on the main thread after N server ticks.
scheduleSync(Runnable, long delayMillis) Waits the delay on the async scheduler, then hops to the main thread.

Waiting for a main-thread result

When an async task needs a value that can only be computed on the main thread, use the SynchronizedAction bridge (mc.getSynchronizedAction()):

McFCScheduler.SynchronizedAction sync = FCScheduler.getMinecraftScheduler().getSynchronizedAction();

// Compute on the main thread and block the caller until it returns.
ItemStack held = sync.runAndGet(() -> player.getInventory().getItemInMainHand());

runAndGet(Callable<T>) runs inline when the caller is already on the main thread; otherwise it posts a FutureTask to the main thread and blocks until it completes. run(Runnable) is the void form. scheduleAndGet(Callable, delayTicks) is the delayed variant and, by contract, must not be called from the main thread (it would deadlock waiting on a tick that cannot advance).

The Hytale side mirrors this through FCScheduler.getHytaleScheduler() (HyFCScheduler). Writing against FCScheduler keeps scheduling code portable across both platforms - see Platform Abstraction.

Async configuration saves

There is no dedicated periodic save thread on this branch. Configuration files persist through EveryConfig's own asynchronous back-store: call config.saveAsync() to hand the write off and return immediately, or config.save() to block until it lands. See Configuration for the save model. Player data is flushed on its own schedule by the storage layer - see PlayerData and PDSections.

Choosing the right thread

  • Blocking work (database, HTTP, disk): FCScheduler.runAsync / scheduleAsync.
  • Recurring background work: FCScheduler.getScheduler().scheduleAtFixedRate(...).
  • Anything touching the Bukkit world/entities/inventories: McFCScheduler.runSync / scheduleSyncInTicks, or a SynchronizedAction when you need the return value.

See also

Clone this wiki locally