Skip to content

Latest commit

 

History

History
356 lines (254 loc) · 12.5 KB

File metadata and controls

356 lines (254 loc) · 12.5 KB

Scheduler API Documentation

The Scheduler API provides Lua scripts with the ability to schedule delayed and repeating tasks.

Threading: runLater and runTimer run their callback on the main server thread, where the Bukkit API is safe to use. runAsync, runLaterAsync and runTimerAsync run it on a scheduler worker thread instead, and runSync brings it back. All scripts share a single Lua interpreter state, so only one callback executes at a time whichever thread it is on; see Asynchronous tasks for what that means in practice.

Execution budget: because callbacks run on the main thread, each one is capped by the script-timeout-ms setting (5 seconds by default; see the Configuration section). Every invocation is a separate entry with its own full budget, so a repeating task is not penalised for how long it has been running overall.

Global Access

The API is accessed via the global SchedulerApi table in Lua scripts.

Methods

runLater

Schedules a task to run after a specified delay.

Syntax:

local taskId = SchedulerApi:runLater(delayTicks, callback)

Parameters:

  • delayTicks (number): Delay in ticks before the task runs (20 ticks = 1 second)
  • callback (function): The function to execute when the task runs

Returns:

  • taskId (string): The task ID (used for cancellation)

Example:

local task = SchedulerApi:runLater(40, function()
    print("This runs after 2 seconds")
end)

runTimer

Schedules a repeating task that runs at a fixed interval.

Syntax:

local taskId = SchedulerApi:runTimer(delayTicks, intervalTicks, callback)

Parameters:

  • delayTicks (number): Initial delay in ticks before the first run
  • intervalTicks (number): Interval in ticks between runs
  • callback (function): The function to execute each time the task runs

Returns:

  • taskId (string): The task ID (used for cancellation)

Example:

local task = SchedulerApi:runTimer(20, 20, function()
    print("This runs every second, starting after 1 second")
end)

runAsync

Runs a callback on a scheduler worker thread as soon as one is free, mirroring Bukkit's runTaskAsynchronously. Read Asynchronous tasks before using this: the callback must not touch the Bukkit API, and it still holds the interpreter lock while it runs.

Syntax:

local taskId = SchedulerApi:runAsync(callback)

Parameters:

  • callback (function): The function to execute off the main thread

Returns:

  • taskId (string): The task ID (used for cancellation)

Example:

SchedulerApi:runAsync(function()
    local contents = readSomeFile()          -- slow work, off the tick loop
    SchedulerApi:runSync(function()
        PlayerApi:sendMessage(player, contents)  -- Bukkit API, back on the main thread
    end)
end)

runLaterAsync

The asynchronous counterpart of runLater, mirroring Bukkit's runTaskLaterAsynchronously.

Syntax:

local taskId = SchedulerApi:runLaterAsync(delayTicks, callback)

Parameters:

  • delayTicks (number): Delay in ticks before the task runs (20 ticks = 1 second)
  • callback (function): The function to execute off the main thread

Returns:

  • taskId (string): The task ID (used for cancellation)

Example:

SchedulerApi:runLaterAsync(100, function()
    print("Runs 5 seconds from now, on a worker thread")
end)

runTimerAsync

The asynchronous counterpart of runTimer, mirroring Bukkit's runTaskTimerAsynchronously.

Syntax:

local taskId = SchedulerApi:runTimerAsync(delayTicks, intervalTicks, callback)

Parameters:

  • delayTicks (number): Initial delay in ticks before the first run
  • intervalTicks (number): Interval in ticks between runs
  • callback (function): The function to execute each time the task runs

Returns:

  • taskId (string): The task ID (used for cancellation)

Example:

SchedulerApi:runTimerAsync(0, 6000, function()
    print("Polls something every 5 minutes without ever costing a tick")
end)

runSync

Runs a callback on the main server thread at the next opportunity, mirroring Bukkit's runTask. This is the way back from an asynchronous task to code that may touch the Bukkit API.

Syntax:

local taskId = SchedulerApi:runSync(callback)

Parameters:

  • callback (function): The function to execute on the main thread

Returns:

  • taskId (string): The task ID (used for cancellation)

Example:

SchedulerApi:runAsync(function()
    local result = expensiveComputation()
    SchedulerApi:runSync(function()
        WorldApi:setBlock(location, result)
    end)
end)

Calling runSync from the main thread still defers the callback to the next tick, exactly as Bukkit's runTask does.


isMainThread

Whether the calling code is running on the main server thread, and may therefore touch the Bukkit API.

Syntax:

local onMain = SchedulerApi:isMainThread()

Returns:

  • onMain (boolean): true on the main server thread, false on a scheduler worker

Example:

local function announce(player, message)
    if SchedulerApi:isMainThread() then
        player:sendMessage(message)
    else
        SchedulerApi:runSync(function() player:sendMessage(message) end)
    end
end

isQueued

Whether a task is still scheduled to run (again).

Syntax:

local queued = SchedulerApi:isQueued(taskId)

Parameters:

  • taskId (string): The task ID

Returns:

  • queued (boolean): false for an unknown, completed or cancelled task

isRunning

Whether a task's callback is executing at this moment. Mainly useful for asynchronous tasks — a synchronous task can only be "running" while you are inside it.

Syntax:

local running = SchedulerApi:isRunning(taskId)

Parameters:

  • taskId (string): The task ID

Returns:

  • running (boolean): false for an unknown, completed or cancelled task

getTasks

The IDs of the calling script's live tasks. Scoped to the caller: a script cannot enumerate another script's tasks.

Syntax:

local ids = SchedulerApi:getTasks()

Returns:

  • ids (table): 1-indexed list of task ID strings

Example:

local ids = SchedulerApi:getTasks()
DebugApi:log("this script has " .. #ids .. " live task(s)")
for _, id in ipairs(ids) do
    if not SchedulerApi:isRunning(id) then
        SchedulerApi:cancelTask(id)
    end
end

Stopping a repeating task from inside it

A repeating callback that returns false cancels its own task. Any other return value, including no return at all, leaves it scheduled.

local ticks = 0
SchedulerApi:runTimerAsync(0, 20, function()
    ticks = ticks + 1
    DebugApi:log("tick " .. ticks)
    if ticks >= 5 then
        return false   -- stop; no task ID needed
    end
end)

This is the recommended way to write a self-limiting timer. Cancelling by ID has an ordering trap: a task with a zero delay can fire before the id has been assigned to the variable the callback closes over, leaving it nil at the moment you need it. Returning false needs no id at all, and works the same for runTimer and runTimerAsync.


Asynchronous tasks

The asynchronous variants exist for the same reason as Bukkit's: work that would otherwise stall the tick loop — waiting on a file, a socket, a database, or a long computation — belongs off the main thread. Two rules come with them, and the second one is specific to Lua.

1. No Bukkit API from an asynchronous callback. This is the ordinary Bukkit rule. Reading or mutating worlds, entities, inventories or players off the main thread corrupts server state. Do the off-thread part in the async callback, then hop back with runSync for anything that touches the game. PdcApi, WorldApi, EntityApi, PlayerApi, InventoryApi and AdventureApi are all main-thread-only.

2. One script runs at a time, even asynchronously. Every script shares a single Lua interpreter, which cannot be entered concurrently, so an asynchronous callback takes the interpreter lock for as long as it runs. The server keeps ticking while it does — that is the win — but any other Lua callback, including event handlers and commands on the main thread, waits behind it. So an async task is the right place to wait on something slow, and the wrong place to spin on a long Lua loop.

Because of rule 2, asynchronous callbacks are subject to the same script-timeout-ms budget as everything else. The budget is what bounds how long one of them can keep the main thread out of the interpreter. Note that the budget is checked between Lua instructions, so it cannot interrupt a callback blocked inside a Java call: a socket read with no timeout of its own holds the lock until it returns, whatever the budget says. Give blocking Java calls their own timeouts.

Things that are safe from an asynchronous callback: the reflection functions (class, newInstance, callStatic, castTo), DebugApi, SchedulerApi itself, and plain Lua.

registerCommand and registerEvent are refused outright from an asynchronous callback — they mutate server-global state — and log an error telling you to wrap the call in runSync. The rest of the main-thread-only APIs are not policed, exactly as in a Java plugin: keeping them out of async callbacks is yours to get right.


cancelTask

Cancels a scheduled task.

Syntax:

local success = SchedulerApi:cancelTask(taskId)

Parameters:

  • taskId (string): The task ID to cancel

Returns:

  • success (boolean): true if the task was cancelled, false if the task was not found or already completed

Example:

local task = SchedulerApi:runTimer(20, 20, function()
    print("Repeating task")
end)
-- Later...
SchedulerApi:cancelTask(task)

cancelAllTasks

Cancels all scheduled tasks created by the calling script. Tasks belonging to other scripts are never affected.

Called from a script's top level, it additionally cancels the tasks left running by the previous version of that same script. This is what makes the usual reload idiom work: a script that schedules a repeating task can call cancelAllTasks on the way in and be sure the old timer is gone, while the tasks it schedules during this load survive that cleanup.

Syntax:

SchedulerApi:cancelAllTasks()

Example:

-- At the top of a script that schedules timers, so /mls reloadscripts
-- does not leave the previous version's timers running alongside the new ones.
SchedulerApi:cancelAllTasks()

SchedulerApi:runTimer(0, 20, function()
    -- ...
end)

Notes

  • All time values are measured in ticks (20 ticks = 1 second)
  • runLater, runTimer and runSync callbacks run on the main server thread, so they can safely access the Bukkit API; the *Async callbacks must not
  • cancelTask and cancelAllTasks work from either kind of callback, and an asynchronous task is cancelled and cleaned up on script unload exactly like a synchronous one
  • Long-running or blocking work in a callback stalls the whole server, exactly as it would in a Java plugin
  • Errors thrown by a callback are logged with a stack trace and do not cancel a repeating task
  • The same applies to a callback aborted for exceeding script-timeout-ms: it is logged and the task stays scheduled, so a repeating task whose callback never finishes logs one abort per repetition until something cancels it. Fix the callback or cancel the task; the watchdog only keeps the server responsive, it does not stop the task from running again
  • A timeout abort cannot be caught with pcall, so cleanup written in an error branch does not run when a callback is aborted
  • Tasks scheduled while a script is loading are attributed to that script, and are kept clear of the cleanup that retires its previous version on reload
  • Task IDs are unique and can be used to cancel specific tasks
  • A repeating callback that returns false cancels its own task; any other return value keeps it scheduled
  • Calling cancelTask on a non-existent or already-completed task returns false, and isQueued/isRunning return false for the same
  • isMainThread, isQueued, isRunning and getTasks are safe to call from either kind of callback
  • There is no equivalent of Bukkit's callSyncMethod: blocking an asynchronous callback on a main-thread result would deadlock, because the callback holds the interpreter lock that the main thread needs to produce it. Use runSync with a closure instead
  • cancelAllTasks cancels all tasks created by the calling script, regardless of their type or state