Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 142 additions & 75 deletions docs/core-features/schedulers.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,104 +4,212 @@ description: Execute tasks synchronously and asynchronously with fluent API.

# Schedulers

## Basic Usage
`Schedulers` provides static access to sync and async `Scheduler` instances. Each `Scheduler` returns `Promise` objects from one-shot methods and `Task` objects from repeating methods, both of which implement `Terminable` for automatic lifecycle management.

### Sync Tasks
## Scheduler access

| Method | Returns | Description |
|---|---|---|
| `Schedulers.sync()` | `Scheduler` | Main server thread |
| `Schedulers.async()` | `Scheduler` | Async thread pool |
| `Schedulers.get(ThreadContext)` | `Scheduler` | Determined by `SYNC` or `ASYNC` context |
| `Schedulers.bukkit()` | `BukkitScheduler` | Raw Bukkit scheduler |
| `Schedulers.builder()` | `TaskBuilder` | Fluent task builder (see below) |

## Scheduler methods

All one-shot methods return `Promise<T>` (or `Promise<Void>` for `run`/`runLater`). Repeating methods return `Task`.

| Method | Parameters | Description |
|---|---|---|
| `run(Runnable)` | runnable | Execute immediately |
| `supply(Supplier<T>)` | supplier | Execute immediately, return value |
| `call(Callable<T>)` | callable | Execute immediately, return value (checked exceptions) |
| `runLater(Runnable, long)` | runnable, delayTicks | Execute after delay |
| `supplyLater(Supplier<T>, long)` | supplier, delayTicks | Execute after delay, return value |
| `callLater(Callable<T>, long)` | callable, delayTicks | Execute after delay, return value |
| `runRepeating(Runnable, long, long)` | runnable, delayTicks, intervalTicks | Repeat on interval |
| `runRepeating(Consumer<Task>, long, long)` | consumer, delayTicks, intervalTicks | Repeat with access to the `Task` handle |

`runLater`, `supplyLater`, `callLater`, and `runRepeating` each have a `TimeUnit` overload (e.g., `runLater(Runnable, long, TimeUnit)`).

## Sync and async task execution

```java
// Run on main thread
Schedulers.sync().run(() -> {
player.teleport(location);
});

// Delayed (20 ticks = 1 second)
Schedulers.sync().runLater(() -> {
player.sendMessage("Delayed message");
}, 20L);

// Repeating (every 20 ticks)
Schedulers.sync().runRepeating(() -> {
player.sendActionBar("Repeating message");
}, 0L, 20L);
```

### Async Tasks

```java
// Run off main thread
Schedulers.async().run(() -> {
// Heavy computation, database queries, etc.
PlayerData data = database.load(uuid);
});

// Delayed async
Schedulers.async().runLater(() -> {
// Heavy async task
database.cleanup();
}, 20L);

// Repeating async
Schedulers.async().runRepeating(() -> {
// Periodic background task
database.heartbeat();
}, 0L, 100L);
```

## Promises

Async computations with callbacks:
All one-shot methods (`run`, `supply`, `call`, and their `*Later` variants) return a `Promise<T>`, which supports chaining across thread contexts.

**Promise chain methods:**

| Method | Runs on | Input | Output |
|---|---|---|---|
| `thenApplySync(Function<V, U>)` | main thread | value | value |
| `thenApplyAsync(Function<V, U>)` | async thread | value | value |
| `thenAcceptSync(Consumer<V>)` | main thread | value | void |
| `thenAcceptAsync(Consumer<V>)` | async thread | value | void |
| `thenRunSync(Runnable)` | main thread | nothing | void |
| `thenRunAsync(Runnable)` | async thread | nothing | void |
| `thenComposeSync(Function<V, Promise<U>>)` | main thread | value | flattened promise |
| `thenComposeAsync(Function<V, Promise<U>>)` | async thread | value | flattened promise |

Each of these also has a delayed variant (e.g., `thenApplyDelayedSync(Function, long)` and `thenApplyDelayedSync(Function, long, TimeUnit)`).

```java
// Supply value asynchronously
Schedulers.async().supply(() -> {
// Heavy computation
return database.loadPlayer(uuid);
}).thenApplySync(data -> {
// Back on main thread
}).thenAcceptSync(data -> {
player.sendMessage("Data loaded: " + data);
return data;
});
```

## Binding Tasks
## TaskBuilder API

`Schedulers.builder()` provides a fluent builder for constructing tasks with explicit thread context and timing.

**Builder chain:**

`Schedulers.builder()` -> `.sync()` or `.async()` -> timing -> terminal

**Timing methods** (on `ThreadContextual`):

| Method | Description |
|---|---|
| `now()` | Execute immediately. Returns `ContextualPromiseBuilder`. |
| `after(long ticks)` | Execute after delay. Returns `DelayedTick`. |
| `after(long, TimeUnit)` | Execute after delay. Returns `DelayedTime`. |
| `every(long ticks)` | Repeat with no initial delay. Returns `ContextualTaskBuilder`. |
| `afterAndEvery(long ticks)` | Delay then repeat at same interval. Returns `ContextualTaskBuilder`. |
| `afterAndEvery(long, TimeUnit)` | Delay then repeat at same interval. Returns `ContextualTaskBuilder`. |

Automatically cancel tasks when plugin disables:
**Terminal methods:**

`ContextualPromiseBuilder` (one-shot): `run(Runnable)`, `supply(Supplier<T>)`, `call(Callable<T>)`

`ContextualTaskBuilder` (repeating): `run(Runnable)`, `consume(Consumer<Task>)`

`DelayedTick`/`DelayedTime` extend `ContextualPromiseBuilder` and add `.every(...)` to convert into a repeating task.

```java
Schedulers.builder()
.async()
.afterAndEvery(5, TimeUnit.MINUTES)
.run(() -> saveAllPlayerData());

Schedulers.builder()
.sync()
.after(60)
.run(() -> player.teleport(spawn));

Schedulers.builder()
.async()
.now()
.supply(() -> database.loadPlayer(uuid))
.thenAcceptSync(data -> applyPlayerData(player, data));
```

## Binding tasks for automatic cleanup

`Task` extends `Terminable`, so repeating tasks can be bound to any `TerminableConsumer`. `Promise` also extends `Terminable`.

```java
Schedulers.sync().runRepeating(() -> {
// This task auto-cancels on plugin disable
updateScoreboard();
}, 0L, 20L).bindWith(this);

Schedulers.builder()
.async()
.every(100)
.run(() -> database.heartbeat())
.bindWith(this);
```

## Tick conversions

The `Ticks` utility converts between ticks and standard `TimeUnit` values.

| Method | Description |
|---|---|
| `Ticks.from(long duration, TimeUnit unit)` | Convert duration to ticks |
| `Ticks.to(long ticks, TimeUnit unit)` | Convert ticks to duration |

| Constant | Value |
|---|---|
| `Ticks.TICKS_PER_SECOND` | 20 |
| `Ticks.MILLISECONDS_PER_SECOND` | 1000 |
| `Ticks.MILLISECONDS_PER_TICK` | 50 |

## Thread safety

The Bukkit API is not thread-safe. Only call Bukkit API methods from the main thread.

```java
// WRONG: Bukkit API from async thread
Schedulers.async().run(() -> {
player.teleport(location);
});

// CORRECT: async computation, sync application
Schedulers.async().supply(() -> {
return database.loadData();
}).thenAcceptSync(data -> {
player.teleport(location);
});
```

## Complete Example
## Complete example

```java
public class MyPlugin extends BasePlugin {

@Override
protected void enable() {
// Load player data async, apply sync
Events.subscribe(PlayerJoinEvent.class)
.handler(e -> {
Player player = e.getPlayer();
UUID uuid = player.getUniqueId();

// Async database load
Schedulers.async().supply(() -> loadPlayerData(uuid))
.thenApplySync(data -> {
// Back on main thread
.thenAcceptSync(data -> {
applyPlayerData(player, data);
Text.tell(player, "&aData loaded!");
return data;
});
})
.bindWith(this);

// Periodic save (every 5 minutes)
Schedulers.async().runRepeating(() -> {
saveAllPlayerData();
}, 0L, 20L * 60 * 5) // 0 delay, 5 minutes interval
Schedulers.builder()
.async()
.afterAndEvery(5, TimeUnit.MINUTES)
.run(() -> saveAllPlayerData())
.bindWith(this);

// Delayed teleport
Events.subscribe(PlayerInteractEvent.class)
.handler(e -> {
Player player = e.getPlayer();
Expand All @@ -110,50 +218,9 @@ public class MyPlugin extends BasePlugin {
Schedulers.sync().runLater(() -> {
player.teleport(spawn);
Text.tell(player, "&aTeleported!");
}, 60L); // 3 seconds = 60 ticks
}, 60L);
})
.bindWith(this);
}

private PlayerData loadPlayerData(UUID uuid) {
// Database query (async safe)
return database.load(uuid);
}

private void applyPlayerData(Player player, PlayerData data) {
// Apply to player (must be on main thread)
player.setLevel(data.getLevel());
}

private void saveAllPlayerData() {
// Save all data (async safe)
database.saveAll();
}
}
```

## Thread Safety

**Important**: Bukkit API is not thread-safe!

```java
// WRONG - Bukkit API from async thread
Schedulers.async().run(() -> {
player.teleport(location); // ERROR! Not thread-safe!
});

// CORRECT - Bukkit API on main thread
Schedulers.async().supply(() -> {
return database.loadData(); // Heavy work async
}).thenApplySync(data -> {
player.teleport(location); // Bukkit API on main thread
return data;
});
```

## Time Units

* **1 tick** = 50ms (1/20th second)
* **20 ticks** = 1 second
* **1200 ticks** = 1 minute
* **72000 ticks** = 1 hour
Loading
Loading