Skip to content

API ActorPool

iKryptonic edited this page May 1, 2026 · 3 revisions

API: ActorPool

ActorPool is a round-robin worker pool for offloading pure compute jobs to Roblox Actor workers.

Important

Docs audit note (2026-05): This page was re-audited against FSM/Orchestrator/Core/ActorPool/init.luau and FSM/Orchestrator/Core/ActorPool/Actor/ActorWorker.server.luau.


🎯 Overview

ActorPool keeps a worker array, picks the next worker in round-robin order, sends it a job message, then blocks the caller until a JobResult reply arrives or a 30-second timeout expires.

Built-in worker jobs in the shipped worker script:

  • SortByKey
  • AggregatePerf

This is for pure data transforms. Do not use it for live Instance access, DataStore I/O, or shared mutable state.


πŸ“¨ Worker protocol

  Caller           ActorPool         Worker Actor        Jobs table
    β”‚                  β”‚                  β”‚                 β”‚
    │─ Submit({ name, input }) ─→│        β”‚                 β”‚
    β”‚                  │─ SendMessage("RunJob", jobId, name, input) ─→│
    β”‚                  β”‚                  β”‚ task.desynchronize()
    β”‚                  β”‚                  │─ Jobs[name](input) ──────→│
    β”‚                  β”‚                  β”‚ task.synchronize()
    β”‚                  │←─ SendMessage("JobResult", jobId, ok, payload) ─│
    │←─ { ok = true, output = payload } or { ok = false, error = ... } ─│

Worker implementation details:

  • ActorWorker.server.luau binds RunJob
  • the job executes inside task.desynchronize()
  • result messaging happens after task.synchronize()
  • unknown jobs error inside pcall and come back as ok = false

πŸ”§ Public types

export type Job = {
    name: string,
    input: any,
}

export type Result = {
    ok: boolean,
    output: any?,
    error: string?,
}

πŸ”§ API reference

ActorPool.new(workers: { Actor }) -> ActorPool

Creates a pool from pre-existing worker actors.

Internal state initialized

  • _workers = workers
  • _rr = 0
  • _nextId = 0
  • _pending = {}

Side effects

  • binds a JobResult message listener on every worker immediately

Important audit note

  • the constructor does not assert that workers is non-empty
  • the empty-pool assertion happens later in _pickWorker() when Submit() runs: assert(#self._workers > 0, "ActorPool has no workers")
local pool = ActorPool.new({
    script.Parent.Worker1,
    script.Parent.Worker2,
    script.Parent.Worker3,
})

ActorPool:Submit(job: Job) -> Result

Dispatches one job to the next worker and blocks until completion or timeout.

Exact flow

  • increments _nextId
  • calls _pickWorker()
  • stores a resolver in _pending[jobId]
  • worker:SendMessage("RunJob", jobId, job.name, job.input)
  • loops with task.wait() until done == true
  • times out after 30 seconds

Returns

  • success: { ok = true, output = payload }
  • worker error: { ok = false, error = tostring(payload) }
  • timeout: { ok = false, error = "Job timed out after 30s" }

Edge cases

  • unknown jobId replies are ignored
  • timeout removes _pending[jobId] to avoid leaking pending resolvers
  • no throw-on-error pattern: failures are always returned in the result object
local result = pool:Submit({
    name = "SortByKey",
    input = {
        list = leaderboardSnapshot,
        key = "Score",
        descending = true,
    },
})

if result.ok then
    print(result.output[1].Score)
else
    warn(result.error)
end

πŸ”© Built-in worker jobs

SortByKey

Input shape:

{
    list: { { [string]: any } },
    key: string,
    descending: boolean,
}

Behavior:

  • clones the list first with table.clone
  • sorts by a[key] / b[key]
  • rows with nil for the key sort to the end
  • descending mode flips the comparator

AggregatePerf

Input shape:

{ samples: { number } }

Output shape:

{ count: number, avg: number }

Behavior:

  • sums numeric samples
  • returns avg = 0 when the list is empty

πŸš€ Real examples

Example 1: Sort leaderboard rows

local result = pool:Submit({
    name = "SortByKey",
    input = {
        list = leaderboardRows,
        key = "Score",
        descending = true,
    },
})

if result.ok then
    local top10 = {}
    for i = 1, math.min(10, #result.output) do
        top10[i] = result.output[i]
    end
end

Example 2: Aggregate frametime samples

local result = pool:Submit({
    name = "AggregatePerf",
    input = { samples = { 14.1, 15.0, 16.3, 14.8 } },
})

if result.ok then
    print(result.output.count, result.output.avg)
end

Example 3: Add a custom pure-compute worker job

-- inside ActorWorker.server.luau
Jobs.NormalizeWeights = function(input)
    local total = 0
    for _, value in ipairs(input.values) do
        total += value
    end
    local out = {}
    for i, value in ipairs(input.values) do
        out[i] = total > 0 and (value / total) or 0
    end
    return out
end

⚠️ Edge cases and pitfalls

  1. No destroy API. Construct a pool once per worker set; repeated new() calls on the same workers add more BindToMessage("JobResult", ...) listeners.

  2. Empty worker list fails only on submit. The constructor itself does not reject an empty array.

  3. Results are always wrapped. Always inspect result.ok before reading result.output.

  4. The built-in worker script is server-only. ActorWorker.server.luau must already be present and running in the actors you pass in.

  5. Do not pass live engine objects. Actor messages should carry serializable data, not gameplay-side mutable objects or external side effects.


πŸ”— See also

🏠 Home


πŸš€ Getting Started


πŸ—οΈ Architecture


πŸ”§ API Reference


πŸ“š Guides


βš™οΈ Advanced


πŸ› οΈ Development


πŸ“– Reference

Clone this wiki locally