search/
Start typing to search packages!
package_2
taskrunner
By @karlobii
Roblox
Mirrored from WallyTaskRunner
A small Wally package that wraps a Luau coroutine with:
- Lifecycle management —
Idle → Running → [Retrying] → Completed | Failed | Cancelled | TimedOut, exposed viaStatusand aStatusChangedsignal. - Priority — a numeric priority you set yourself, honored by the bundled
Scheduler(highest priority runs first, FIFO among ties). - Timeout awareness — a hard watchdog that cancels the underlying thread, plus a
Handleyour function can poll cooperatively (handle:CheckTimeout(),handle:TimeLeft()). - Automatic retry — configurable attempt count, delay, exponential backoff, jitter, and an optional
shouldRetry(err, attempt)predicate. - Structured result/error propagation — every run ends in a
Result<T>table ({ ok, value, error, attempts, elapsed }) instead of a barepcallboolean.
Installation
# wally.toml
[dependencies]
TaskRunner = "karlobii/taskrunner@0.1.0"
Quick start
local TaskRunner = require(Packages.TaskRunner)
local job = TaskRunner.new(function(handle)
handle:CheckTimeout() -- bail out early if we're already over time/cancelled
local response = httpService:RequestAsync({ Url = "https://example.com" })
if response.StatusCode ~= 200 then
error("bad status " .. response.StatusCode)
end
return response.Body
end, {
name = "FetchExample",
priority = TaskRunner.Priority.High,
timeout = 5, -- seconds
retries = 3, -- retry up to 3 times after the first attempt
retryDelay = 0.5, -- base delay between attempts
backoff = 2, -- exponential backoff multiplier
jitter = 0.25, -- up to +0.25s of random jitter added to each delay
})
job.Completed:Connect(function(result)
print("got body:", result.value)
end)
job.Failed:Connect(function(result)
warn(("failed after %d attempts: %s"):format(result.attempts, result.error))
end)
job:Start()
Or synchronously from another coroutine/thread:
local result = TaskRunner.new(fn, { timeout = 3 }):Start():Await()
if result.ok then
print(result.value)
else
warn(result.error)
end
API
TaskRunner.new(fn, options?) -> TaskRunnerInstance
fn is called as fn(handle). Its return value becomes Result.value on success; any error thrown becomes Result.error on failure (after retries are exhausted).
options:
| Field | Type | Default | Description |
|---|---|---|---|
name | string | "TaskRunner" | Label, useful in logs/errors. |
priority | number | TaskRunner.Priority.Normal | Consumed by Scheduler; otherwise informational. |
timeout | number? | nil (no timeout) | Seconds before the attempt is force-cancelled. |
retries | number | 0 | Additional attempts after the first failure. |
retryDelay | number | 0 | Base delay in seconds before a retry. |
backoff | number | 1 | Multiplier applied to retryDelay per attempt (retryDelay * backoff^(attempt-1)). |
jitter | number | 0 | Adds random() * jitter extra seconds to each retry delay. |
shouldRetry | (err: string, attempt: number) -> boolean | nil | Return false to stop retrying regardless of retries left. |
Instance members
Status— one ofTaskRunner.Status.{Idle, Running, Retrying, Completed, Failed, Cancelled, TimedOut}.Attempts— number of attempts made so far.Result— the lastResult<T>once terminal, elsenil.Completed,Failed,Cancelled—Signal<Result<T>>.Retrying—Signal<attempt: number, error: string>, fired before each retry sleep.StatusChanged—Signal<newStatus, oldStatus>.:Start() -> self— begins execution; can only be called once.:Await() -> Result<T>— cooperatively waits (viatask.wait) until terminal, then returnsResult.:Cancel(reason: string?)— cancels immediately from any non-terminal state.:IsDone() -> boolean.
Handle (passed into your function)
handle:IsCancelled() -> booleanhandle:TimeLeft() -> number?— seconds until the deadline, ornilif no timeout is set.handle:CheckTimeout()— throws if cancelled or past the deadline; call this at loop boundaries in long-running work so timeouts/cancellation are cooperative rather than only enforced by the hard watchdog.handle.Attempt— the current attempt number (1on the first try).
Result<T>
{
ok: boolean,
value: T?, -- present when ok == true
error: string?, -- present when ok == false
attempts: number,
elapsed: number, -- seconds since :Start()
}
TaskRunner.Scheduler
Optional bounded-concurrency priority queue for running many TaskRunners together.
local scheduler = TaskRunner.Scheduler.new({ maxConcurrent = 4 })
scheduler:Add(jobA)
scheduler:Add(jobB) -- higher-priority jobs run first regardless of add order
scheduler.Idle:Connect(function()
print("queue drained")
end)
Scheduler.new({ maxConcurrent: number? }) -> Scheduler:Add(runner) -> runner— queues an un-startedTaskRunner; starts it once a concurrency slot frees up.:CancelAll(reason: string?)— cancels every queued and in-flight runner.:RunningCount(),:QueuedCount()..Idle—Signal<>, fires whenever the queue empties and nothing is running.
Design notes
- The hard timeout is enforced with
task.cancelon the underlying coroutine's thread. This works for any coroutine that yields periodically — e.g. one that callstask.waitin a loop — even if it never callshandle:CheckTimeout(). It cannot interrupt a coroutine that never yields at all (a tight, non-yielding CPU loop): Luau's cooperative scheduling means nothing can preempt code that never hands control back. If your work might spin without yielding, callhandle:CheckTimeout()inside the loop yourself so it can bail out cooperatively. - Retries reuse the same
TaskRunnerInstance—Attemptsaccumulates across the whole lifecycle, andResult.attemptsreflects the total attempts made when the runner settles. TaskRunner:Start()is one-shot by design; construct a fresh instance if you need to run the same logic again.
Package Details
Install command (Click to copy)
Version
0.1.0
License
MIT
Safe for commercial use
Automated license review — not legal advice.
