Skip to content

Commit 8f2eea3

Browse files
authored
GH-71 Add loom schedulers for Java 21+, Bukkit, and Folia modules (#71)
* Add loom schedulers for Java 21+, Bukkit, and Folia modules * Follow review. * Fix.
1 parent de03eab commit 8f2eea3

File tree

29 files changed

+3126
-9
lines changed

29 files changed

+3126
-9
lines changed

.github/workflows/gradle.yml

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,14 @@ on:
99
jobs:
1010
build:
1111
runs-on: ubuntu-latest
12-
strategy:
13-
matrix:
14-
java:
15-
- 17
16-
fail-fast: false
1712
steps:
1813
- name: Checkout
1914
uses: actions/checkout@v6
20-
- name: 'Set up JDK ${{ matrix.java }}'
15+
- name: Set up JDK 21
2116
uses: actions/setup-java@v5
2217
with:
23-
distribution: adopt
24-
java-version: '${{ matrix.java }}'
18+
distribution: temurin
19+
java-version: '21'
2520
- name: Cache Gradle
2621
uses: actions/cache@v5
2722
with:

.github/workflows/publish-release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
uses: actions/setup-java@v5
1616
with:
1717
distribution: 'temurin'
18-
java-version: 17
18+
java-version: 21
1919
cache: 'gradle'
2020

2121
- name: Grant execute permission for gradlew
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
plugins {
2+
`java-library`
3+
}
4+
5+
java {
6+
sourceCompatibility = JavaVersion.VERSION_21
7+
targetCompatibility = JavaVersion.VERSION_21
8+
}
9+
10+
tasks.compileJava {
11+
options.encoding = "UTF-8"
12+
}
13+
14+
tasks.compileTestJava {
15+
options.encoding = "UTF-8"
16+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# eternalcode-commons-bukkit-loom
2+
3+
Virtual Thread scheduler for Bukkit/Paper plugins.
4+
5+
## Dependency
6+
7+
```kotlin
8+
// Gradle
9+
implementation("com.eternalcode:eternalcode-commons-bukkit-loom:1.3.1")
10+
```
11+
12+
```xml
13+
<!-- Maven -->
14+
<dependency>
15+
<groupId>com.eternalcode</groupId>
16+
<artifactId>eternalcode-commons-bukkit-loom</artifactId>
17+
<version>1.3.1</version>
18+
</dependency>
19+
```
20+
21+
Repository: `https://repo.eternalcode.pl/releases`
22+
23+
## Requirements
24+
25+
- Java 21+
26+
- Paper/Spigot 1.19+
27+
- **Not for Folia** - use `folia-loom` instead
28+
29+
## Usage
30+
31+
```java
32+
public class MyPlugin extends JavaPlugin {
33+
private BukkitLoomScheduler scheduler;
34+
35+
@Override
36+
public void onEnable() {
37+
this.scheduler = BukkitLoomScheduler.create(this);
38+
}
39+
40+
@Override
41+
public void onDisable() {
42+
this.scheduler.shutdown(Duration.ofSeconds(5));
43+
}
44+
45+
public void loadData(Player player) {
46+
scheduler.supplyAsync(() -> database.load(player.getUniqueId()))
47+
.thenAcceptSync(data -> {
48+
// safe - main thread
49+
player.sendMessage("Loaded: " + data);
50+
})
51+
.exceptionally(e -> getLogger().severe(e.getMessage()));
52+
}
53+
}
54+
```
55+
56+
## Rules
57+
58+
- `Async` methods → Virtual Thread (no Bukkit API!)
59+
- `Sync` methods → Main Thread (Bukkit API safe)
60+
- Use `.thenAcceptSync()` to switch from async to sync
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
plugins {
2+
`commons-java-21`
3+
`commons-publish`
4+
`commons-repositories`
5+
}
6+
7+
sourceSets {
8+
main {
9+
java.setSrcDirs(listOf("src/main/java"))
10+
}
11+
}
12+
13+
dependencies {
14+
api(project(":eternalcode-commons-loom"))
15+
16+
compileOnly("org.spigotmc:spigot-api:1.19.4-R0.1-SNAPSHOT")
17+
18+
api("org.jetbrains:annotations:26.0.2-1")
19+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package com.eternalcode.commons.bukkit.scheduler;
2+
3+
import com.eternalcode.commons.scheduler.loom.LoomFuture;
4+
import com.eternalcode.commons.scheduler.loom.LoomScheduler;
5+
import com.eternalcode.commons.scheduler.loom.LoomSchedulerImpl;
6+
import com.eternalcode.commons.scheduler.loom.LoomTask;
7+
import org.bukkit.plugin.Plugin;
8+
9+
import java.time.Duration;
10+
import java.util.concurrent.Callable;
11+
import java.util.function.Consumer;
12+
import java.util.function.Function;
13+
import java.util.function.Supplier;
14+
15+
/**
16+
* Bukkit wrapper for LoomScheduler.
17+
* Create in onEnable(), shutdown in onDisable().
18+
*/
19+
public final class BukkitLoomScheduler implements LoomScheduler {
20+
21+
private final BukkitMainThreadDispatcher dispatcher;
22+
private final LoomSchedulerImpl delegate;
23+
private final Plugin plugin;
24+
25+
private BukkitLoomScheduler(Plugin plugin, BukkitMainThreadDispatcher dispatcher) {
26+
this.plugin = plugin;
27+
this.dispatcher = dispatcher;
28+
this.delegate = new LoomSchedulerImpl(dispatcher);
29+
}
30+
31+
public static BukkitLoomScheduler create(Plugin plugin) {
32+
if (!plugin.getServer().isPrimaryThread()) {
33+
throw new IllegalStateException("BukkitLoomScheduler must be created on main thread");
34+
}
35+
return new BukkitLoomScheduler(plugin, new BukkitMainThreadDispatcher(plugin));
36+
}
37+
38+
@Override
39+
public LoomTask runAsync(Runnable task) {
40+
return this.delegate.runAsync(task);
41+
}
42+
43+
@Override
44+
public <T> LoomFuture<T> supplyAsync(Supplier<T> supplier) {
45+
return this.delegate.supplyAsync(supplier);
46+
}
47+
48+
@Override
49+
public <T> LoomFuture<T> callAsync(Callable<T> callable) {
50+
return this.delegate.callAsync(callable);
51+
}
52+
53+
@Override
54+
public LoomTask runAsyncLater(Runnable task, Duration delay) {
55+
return this.delegate.runAsyncLater(task, delay);
56+
}
57+
58+
@Override
59+
public LoomTask runAsyncTimer(Runnable task, Duration delay, Duration period) {
60+
return this.delegate.runAsyncTimer(task, delay, period);
61+
}
62+
63+
@Override
64+
public LoomTask runSync(Runnable task) {
65+
return this.delegate.runSync(task);
66+
}
67+
68+
@Override
69+
public <T> LoomFuture<T> supplySync(Supplier<T> supplier) {
70+
return this.delegate.supplySync(supplier);
71+
}
72+
73+
@Override
74+
public LoomTask runSyncLater(Runnable task, Duration delay) {
75+
return this.delegate.runSyncLater(task, delay);
76+
}
77+
78+
@Override
79+
public LoomTask runSyncTimer(Runnable task, Duration delay, Duration period) {
80+
return this.delegate.runSyncTimer(task, delay, period);
81+
}
82+
83+
@Override
84+
public <T> LoomTask runAsyncThenSync(Supplier<T> asyncSupplier, Consumer<T> syncConsumer) {
85+
return this.delegate.runAsyncThenSync(asyncSupplier, syncConsumer);
86+
}
87+
88+
@Override
89+
public <T, R> LoomTask runAsyncThenSync(Supplier<T> asyncSupplier, Function<T, R> transformer,
90+
Consumer<R> syncConsumer) {
91+
return this.delegate.runAsyncThenSync(asyncSupplier, transformer, syncConsumer);
92+
}
93+
94+
@Override
95+
public LoomFuture<Void> delay(Duration duration) {
96+
return this.delegate.delay(duration);
97+
}
98+
99+
@Override
100+
public boolean isMainThread() {
101+
return this.dispatcher.isMainThread();
102+
}
103+
104+
@Override
105+
public boolean shutdown(Duration timeout) {
106+
this.dispatcher.shutdown();
107+
return this.delegate.shutdown(timeout);
108+
}
109+
110+
@Override
111+
public void shutdownNow() {
112+
this.dispatcher.shutdown();
113+
this.delegate.shutdownNow();
114+
}
115+
116+
public Plugin getPlugin() {
117+
return this.plugin;
118+
}
119+
120+
public int getPendingSyncTasks() {
121+
return this.dispatcher.getPendingCount();
122+
}
123+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package com.eternalcode.commons.bukkit.scheduler;
2+
3+
import com.eternalcode.commons.scheduler.loom.MainThreadDispatcher;
4+
import org.bukkit.plugin.Plugin;
5+
import org.bukkit.scheduler.BukkitScheduler;
6+
import org.bukkit.scheduler.BukkitTask;
7+
8+
import java.util.concurrent.ConcurrentLinkedQueue;
9+
10+
/**
11+
* Bukkit implementation - queues tasks from VT to main thread.
12+
*/
13+
public final class BukkitMainThreadDispatcher implements MainThreadDispatcher {
14+
15+
private final ConcurrentLinkedQueue<Runnable> queue = new ConcurrentLinkedQueue<>();
16+
private final Plugin plugin;
17+
private final BukkitScheduler bukkitScheduler;
18+
private final BukkitTask tickTask;
19+
20+
public BukkitMainThreadDispatcher(Plugin plugin) {
21+
this.plugin = plugin;
22+
this.bukkitScheduler = plugin.getServer().getScheduler();
23+
this.tickTask = this.bukkitScheduler.runTaskTimer(this.plugin, this::drainQueue, 1L, 1L);
24+
}
25+
26+
private void drainQueue() {
27+
Runnable task;
28+
while ((task = this.queue.poll()) != null) {
29+
try {
30+
task.run();
31+
} catch (Throwable t) {
32+
this.plugin.getLogger().severe("Exception in sync task: " + t.getMessage());
33+
t.printStackTrace();
34+
}
35+
}
36+
}
37+
38+
@Override
39+
public void dispatch(Runnable task) {
40+
if (isMainThread()) {
41+
try {
42+
task.run();
43+
} catch (Throwable t) {
44+
this.plugin.getLogger().severe("Exception in sync task: " + t.getMessage());
45+
t.printStackTrace();
46+
}
47+
return;
48+
}
49+
this.queue.offer(task);
50+
}
51+
52+
@Override
53+
public boolean isMainThread() {
54+
return this.plugin.getServer().isPrimaryThread();
55+
}
56+
57+
@Override
58+
public void dispatchLater(Runnable task, long ticks) {
59+
this.bukkitScheduler.runTaskLater(this.plugin, task, ticks);
60+
}
61+
62+
@Override
63+
public Cancellable dispatchTimer(Runnable task, long delay, long period) {
64+
BukkitTask t = this.bukkitScheduler.runTaskTimer(this.plugin, task, delay, period);
65+
return t::cancel;
66+
}
67+
68+
public void shutdown() {
69+
this.tickTask.cancel();
70+
Runnable task;
71+
while ((task = this.queue.poll()) != null) {
72+
try {
73+
task.run();
74+
} catch (Throwable t) {
75+
this.plugin.getLogger().severe("Exception in shutdown task: " + t.getMessage());
76+
}
77+
}
78+
}
79+
80+
public int getPendingCount() {
81+
return this.queue.size();
82+
}
83+
}

0 commit comments

Comments
 (0)