Skip to content

Conversation

@vLuckyyy
Copy link
Member

@vLuckyyy vLuckyyy commented Jan 1, 2026

No description provided.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @vLuckyyy, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the project's asynchronous task management capabilities by introducing a new set of modules leveraging Java 21's Virtual Threads. It provides a robust, platform-agnostic core scheduler and specialized integrations for Bukkit/Paper and Folia, addressing the unique threading models of each environment. This allows developers to write highly concurrent code more easily and safely, improving performance and responsiveness in Minecraft server plugins.

Highlights

  • New Loom Scheduler Modules: Introduced three new modules: eternalcode-commons-loom (core, platform-agnostic Virtual Thread scheduler for Java 21+), eternalcode-commons-bukkit-loom (integration for Bukkit/Paper), and eternalcode-commons-folia-loom (integration for Folia with region-aware dispatching).
  • Java 21 Compatibility: Added a new Gradle Kotlin script (commons-java-21.gradle.kts) to standardize Java 21 source and target compatibility across projects.
  • Core Loom Scheduler Functionality: The eternalcode-commons-loom module provides core interfaces and implementations for asynchronous (Virtual Thread) and synchronous (main thread) task scheduling, including LoomFuture for chained operations with context switching, LoomTask for task management, and VirtualThreadExecutor for managing Virtual Threads.
  • Bukkit/Paper Integration: The eternalcode-commons-bukkit-loom module offers BukkitLoomScheduler and BukkitMainThreadDispatcher to seamlessly integrate Virtual Thread scheduling with Bukkit's single-main-thread model, ensuring Bukkit API calls are safely dispatched to the main thread.
  • Folia Integration with Region-Aware Dispatching: The eternalcode-commons-folia-loom module provides FoliaLoomScheduler with specialized dispatchers (FoliaEntityDispatcher, FoliaGlobalDispatcher, FoliaRegionDispatcher) to handle Folia's multi-threaded, region-aware architecture, allowing sync tasks to run on the correct entity, location, or global region threads.
  • Comprehensive Unit Testing: Extensive unit tests have been added for the core eternalcode-commons-loom module, covering LoomFuture, LoomSchedulerImpl, LoomTask, MainThreadDispatcher, QueuedDispatcher, and VirtualThreadExecutor to ensure robustness and correctness.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces an impressive and well-structured set of Loom-based schedulers for Java 21+, with specific support for Bukkit and Folia. The separation between the platform-agnostic core and platform-specific modules is clean, and the extensive unit tests are commendable. My review focuses on improving the platform-agnosticism of the core scheduler, fixing a cancellability bug in delayed tasks, and enhancing robustness in the dispatcher implementations. Overall, this is a high-quality addition.

Comment on lines +127 to +144
public LoomTask runSyncLater(Runnable task, Duration delay) {
if (this.shutdown.get()) {
return LoomTask.EMPTY;
}
long ticks = delay.toMillis() / 50L;
CompletableFuture<Void> future = new CompletableFuture<>();
this.dispatcher.dispatchLater(
() -> {
try {
task.run();
future.complete(null);
}
catch (Throwable t) {
future.completeExceptionally(t);
}
}, ticks);
return new SimpleLoomTask(future, false, false);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This implementation of runSyncLater has two issues:

  1. Platform Dependency: It uses a hardcoded tick duration (delay.toMillis() / 50L), which assumes a 20 TPS server. This makes the loom core module dependent on a Bukkit-specific concept.
  2. Cancellability Bug: The returned LoomTask is not truly cancellable. Calling cancel() on it will not prevent the underlying scheduled task from running.

A better approach is to use the VirtualThreadExecutor for the delay, which is platform-agnostic and properly cancellable. The delayed task can then dispatch the work to the MainThreadDispatcher.

    public LoomTask runSyncLater(Runnable task, Duration delay) {
        if (this.shutdown.get()) {
            return LoomTask.EMPTY;
        }
        VirtualThreadExecutor.DelayedTask dt = this.vtExecutor.submitDelayed(() -> this.dispatcher.dispatch(task), delay);
        return new DelayedLoomTask(dt, false, false);
    }

Comment on lines +147 to +155
public LoomTask runSyncTimer(Runnable task, Duration delay, Duration period) {
if (this.shutdown.get()) {
return LoomTask.EMPTY;
}
long delayTicks = delay.toMillis() / 50L;
long periodTicks = period.toMillis() / 50L;
MainThreadDispatcher.Cancellable c = this.dispatcher.dispatchTimer(task, delayTicks, periodTicks);
return new CancellableLoomTask(c, false, true);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This implementation of runSyncTimer is not platform-agnostic because it converts Duration to ticks using a hardcoded value of 50ms per tick (delay.toMillis() / 50L). This assumes a 20 TPS server, which ties the core loom module to a Bukkit concept.

To make this method platform-agnostic, you can use the VirtualThreadExecutor for timing and then dispatch the task to the main thread.

    public LoomTask runSyncTimer(Runnable task, Duration delay, Duration period) {
        if (this.shutdown.get()) {
            return LoomTask.EMPTY;
        }
        VirtualThreadExecutor.DelayedTask dt = this.vtExecutor.submitRepeating(() -> this.dispatcher.dispatch(task), delay, period);
        return new DelayedLoomTask(dt, false, true);
    }

Comment on lines +69 to +71
if (this.tickTask != null) {
this.tickTask.cancel();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The tickTask field (line 18) can be made final as it's initialized in the constructor and never changed, which improves immutability. If you make it final, this null check is no longer necessary.

        this.tickTask.cancel();

try {
task.run();
}
catch (Throwable ignored) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Swallowing exceptions silently, even during shutdown, can hide potential problems. It's better to log the exception, similar to how it's handled in drainQueue() or in BukkitMainThreadDispatcher. This ensures that any issues during the final execution of tasks are not lost.

            catch (Throwable t) {
                this.plugin.getLogger().severe("Exception in global shutdown task: " + t.getMessage());
            }

@Jakubk15
Copy link
Member

Jakubk15 commented Jan 3, 2026

@gemini-code-assist What makes virtual threads better in this project? Are loom schedulers considered a drop-in replacement for the schedulers we've been using so far (traditional multithreading)? Who should use them, and who should stick to the traditional multithreading approach?

@gemini-code-assist
Copy link
Contributor

That's an excellent set of questions, @Jakubk15! Let's break down the advantages and considerations for using Virtual Threads (Loom Schedulers) in this project, especially in the context of Minecraft server development with Bukkit/Paper and Folia.

What makes virtual threads better in this project?

Virtual Threads, introduced in Java 21 as part of Project Loom, offer several significant advantages over traditional platform threads, particularly for I/O-bound applications like many server plugins:

  1. Massive Concurrency: Virtual Threads are extremely lightweight. You can create millions of them, whereas creating thousands of traditional platform threads would quickly exhaust system resources. This means the server can handle a much larger number of concurrent tasks (e.g., player data loading, API calls, database queries) without the overhead of managing a large pool of expensive platform threads.
  2. Simplified Asynchronous Programming: They allow developers to write asynchronous, non-blocking code in a synchronous, blocking style. Instead of complex CompletableFuture chains or callback hell, you can write sequential code that sleeps or waits for I/O, and the Virtual Thread will automatically unmount from its carrier thread, freeing up the carrier for other tasks. This significantly improves code readability and maintainability.
  3. Reduced Resource Consumption: Because they are so lightweight, Virtual Threads consume far less memory per thread than platform threads. This can lead to lower memory usage for applications that rely heavily on concurrency.
  4. Improved Responsiveness: By offloading blocking operations (like database access or network requests) to Virtual Threads, the main server thread (or region threads in Folia) remains free to process game ticks, leading to a more responsive and less laggy server experience.

Are Loom Schedulers considered a drop-in replacement for the schedulers we've been using so far (traditional multithreading)?

Not entirely a "drop-in" replacement, but they are designed to make the transition as smooth as possible. Here's why:

  • Simplified API: The LoomScheduler interface and LoomFuture class in this PR provide a familiar CompletableFuture-like API, but with built-in context switching. This means you can chain asynchronous (Virtual Thread) operations with synchronous (main thread/region thread) operations seamlessly.
  • Integration Layer: For platforms like Bukkit/Paper and Folia, direct interaction with Virtual Threads for game-state modifications is unsafe. This project's BukkitLoomScheduler and FoliaLoomScheduler modules act as crucial integration layers. They ensure that any tasks requiring access to the Bukkit API are automatically dispatched back to the correct main thread (for Bukkit/Paper) or region-specific thread (for Folia), while blocking operations run safely on Virtual Threads.
  • Migration Effort: While the API is designed to be intuitive, migrating existing code that uses traditional BukkitScheduler or CompletableFuture with custom ExecutorService instances will require some refactoring to use the new LoomScheduler methods (supplyAsync, thenAcceptSync, etc.). The core logic of your tasks might remain similar, but the scheduling and thread management calls will change.

Who should use them, and who should stick to the traditional multithreading approach?

You should use Loom Schedulers if:

  • You are on Java 21+: Virtual Threads are a Java 21+ feature.
  • Your plugin performs frequent I/O operations: This includes database queries, HTTP requests to external APIs, file I/O, or any operation that might cause a thread to block while waiting for a result. Virtual Threads excel at making these operations non-blocking from the perspective of the carrier thread.
  • You want to simplify complex asynchronous logic: If you find yourself struggling with nested callbacks or overly complex CompletableFuture chains to manage concurrency, Virtual Threads can significantly clean up your code by allowing you to write sequential-looking code for asynchronous tasks.
  • You are developing for Folia: Folia's multi-threaded region architecture makes traditional BukkitScheduler usage more complex. The FoliaLoomScheduler simplifies this by providing region-aware dispatchers, ensuring your sync tasks run on the correct region thread without manual thread checks.
  • You want to improve server performance and responsiveness: By efficiently handling blocking operations, Virtual Threads can reduce server lag caused by long-running tasks blocking game threads.

You might stick to traditional multithreading (or use it alongside Loom Schedulers) if:

  • You are on an older Java version: Virtual Threads are not available before Java 21.
  • Your tasks are primarily CPU-bound: For tasks that involve heavy computation and don't frequently block on I/O, the benefits of Virtual Threads over a well-tuned ForkJoinPool (used by CompletableFuture's default async methods) might be less pronounced. However, even for CPU-bound tasks, Virtual Threads can still offer simpler programming models.
  • You have a small, simple plugin with minimal concurrency needs: For very basic plugins that only schedule a few simple tasks, the overhead of introducing a new scheduling framework might not be worth the benefit.
  • You have an existing, stable codebase that would be too costly to refactor: While the benefits are clear, refactoring a large, complex plugin might be a significant undertaking. In such cases, a gradual adoption or sticking with the existing approach might be more practical in the short term.

In summary, for new development or significant refactoring in Java 21+ Minecraft server plugins, especially those with I/O-heavy operations or targeting Folia, Loom Schedulers offer a powerful and elegant solution to concurrency challenges.

@Jakubk15
Copy link
Member

Jakubk15 commented Jan 3, 2026

@gemini-code-assist Can you provide an example of how a sample fragment of asynchronous code would look before/after the refactoring to loom schedulers?

@gemini-code-assist
Copy link
Contributor

That's a great request, @Jakubk15! Providing a concrete example will definitely help illustrate the benefits. Let's consider a common scenario in Minecraft plugin development: loading player data from a database (an asynchronous, I/O-bound operation) and then sending a message to the player (a synchronous, main-thread-bound operation).

Before: Traditional Multithreading (e.g., using BukkitScheduler or CompletableFuture)

Before Loom Schedulers, you might have used BukkitScheduler for asynchronous tasks, often combined with CompletableFuture for more complex chains. The key challenge is safely switching back to the main thread for Bukkit API calls.

import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class OldSchedulerExample extends JavaPlugin {

    private final ExecutorService databaseExecutor = Executors.newCachedThreadPool();

    @Override
    public void onDisable() {
        databaseExecutor.shutdown();
    }

    public void loadAndDisplayPlayerData(Player player) {
        UUID playerId = player.getUniqueId();

        // Asynchronous database loading on a separate thread
        CompletableFuture.supplyAsync(() -> {
            // Simulate database call
            getLogger().info("Loading data for " + playerId + " on thread: " + Thread.currentThread().getName());
            try {
                Thread.sleep(1000); // Simulate I/O delay
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            return "Player data for " + playerId;
        }, databaseExecutor)
        .thenAccept(data -> {
            // Switch back to the main thread for Bukkit API calls
            Bukkit.getScheduler().runTask(this, () -> {
                getLogger().info("Displaying data for " + playerId + " on thread: " + Thread.currentThread().getName());
                player.sendMessage("Loaded: " + data);
            });
        })
        .exceptionally(e -> {
            getLogger().severe("Error loading data: " + e.getMessage());
            return null;
        });
    }
}

Explanation of the "Before" code:

  • We use a CompletableFuture with a custom ExecutorService (databaseExecutor) to perform the potentially blocking database operation asynchronously. This prevents the main server thread from freezing.
  • After the data is loaded, we need to explicitly switch back to the main thread using Bukkit.getScheduler().runTask(this, ...) before interacting with the Player object (e.g., player.sendMessage()), as Bukkit API calls are generally not thread-safe outside the main thread.
  • Error handling is done via .exceptionally().

After: Using Loom Schedulers

With Loom Schedulers, the code becomes more streamlined and readable, as the context switching is handled by the LoomFuture chain itself.

import com.eternalcode.commons.bukkit.scheduler.BukkitLoomScheduler;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import java.time.Duration;
import java.util.UUID;

public class LoomSchedulerExample extends JavaPlugin {

    private BukkitLoomScheduler scheduler;

    @Override
    public void onEnable() {
        this.scheduler = BukkitLoomScheduler.create(this);
    }

    @Override
    public void onDisable() {
        this.scheduler.shutdown(Duration.ofSeconds(5));
    }

    public void loadAndDisplayPlayerData(Player player) {
        UUID playerId = player.getUniqueId();

        scheduler.supplyAsync(() -> {
            // This runs on a Virtual Thread (async)
            getLogger().info("Loading data for " + playerId + " on thread: " + Thread.currentThread().getName());
            try {
                Thread.sleep(1000); // Simulate I/O delay
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            return "Player data for " + playerId;
        })
        .thenAcceptSync(data -> {
            // This automatically switches to the main thread (sync)
            getLogger().info("Displaying data for " + playerId + " on thread: " + Thread.currentThread().getName());
            player.sendMessage("Loaded: " + data);
        })
        .exceptionally(e -> {
            getLogger().severe("Error loading data: " + e.getMessage());
            return null;
        });
    }
}

Explanation of the "After" code:

  • We initialize BukkitLoomScheduler in onEnable() and shut it down in onDisable().
  • The supplyAsync() method automatically executes the provided Supplier on a Virtual Thread. This is where your blocking I/O operations (like database calls) should go.
  • The magic happens with .thenAcceptSync(). This method ensures that the subsequent Consumer (which interacts with the Bukkit API) is automatically dispatched and executed on the main server thread. You no longer need explicit Bukkit.getScheduler().runTask() calls.
  • The code flow reads much more linearly, almost as if it were synchronous, while still maintaining asynchronous execution for blocking operations and thread safety for Bukkit API calls.
  • Error handling remains similar with .exceptionally().

As you can see, the Loom Schedulers significantly simplify the asynchronous programming model, especially when dealing with mixed async/sync operations and platform-specific threading requirements like those in Bukkit/Paper and Folia.

this.plugin.getLogger().severe("Exception in entity task: " + ex.getMessage());
ex.printStackTrace();
}
}, null); // null = entity retired, task won't run
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}, null); // null = entity retired, task won't run
}, null); // null = no task will run once entity retires

public FoliaGlobalDispatcher(Plugin plugin) {
this.plugin = plugin;
this.globalScheduler = plugin.getServer().getGlobalRegionScheduler();
this.tickTask = this.globalScheduler.runAtFixedRate(this.plugin, t -> drainQueue(), 1L, 1L);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can use method reference this::drainQueue here.

}

@Override
public java.util.concurrent.Future<?> asFuture() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why no import?

*/
public interface LoomTask {

LoomTask EMPTY = new LoomTask() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add @TestOnly annotation

interface Cancellable {
void cancel();
}
void dispatch(Runnable task);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this up

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants