-
Notifications
You must be signed in to change notification settings - Fork 0
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.
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:
SortByKeyAggregatePerf
This is for pure data transforms. Do not use it for live Instance access, DataStore I/O, or shared mutable state.
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.luaubindsRunJob - the job executes inside
task.desynchronize() - result messaging happens after
task.synchronize() - unknown jobs error inside
pcalland come back asok = false
export type Job = {
name: string,
input: any,
}
export type Result = {
ok: boolean,
output: any?,
error: string?,
}Creates a pool from pre-existing worker actors.
Internal state initialized
_workers = workers_rr = 0_nextId = 0_pending = {}
Side effects
- binds a
JobResultmessage listener on every worker immediately
Important audit note
- the constructor does not assert that
workersis non-empty - the empty-pool assertion happens later in
_pickWorker()whenSubmit()runs:assert(#self._workers > 0, "ActorPool has no workers")
local pool = ActorPool.new({
script.Parent.Worker1,
script.Parent.Worker2,
script.Parent.Worker3,
})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()untildone == 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
jobIdreplies 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)
endInput shape:
{
list: { { [string]: any } },
key: string,
descending: boolean,
}Behavior:
- clones the list first with
table.clone - sorts by
a[key]/b[key] - rows with
nilfor the key sort to the end - descending mode flips the comparator
Input shape:
{ samples: { number } }Output shape:
{ count: number, avg: number }Behavior:
- sums numeric samples
- returns
avg = 0when the list is empty
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
endlocal 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-- 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-
No destroy API. Construct a pool once per worker set; repeated
new()calls on the same workers add moreBindToMessage("JobResult", ...)listeners. -
Empty worker list fails only on submit. The constructor itself does not reject an empty array.
-
Results are always wrapped. Always inspect
result.okbefore readingresult.output. -
The built-in worker script is server-only.
ActorWorker.server.luaumust already be present and running in the actors you pass in. -
Do not pass live engine objects. Actor messages should carry serializable data, not gameplay-side mutable objects or external side effects.
Quick Links: Home Β· Quick Start Β· API Reference Β· Architecture Β· Examples Β· Glossary
Copyright: Β© 2026 RBXStateMachine contributors Β· Repository Β· License information