SCUA 0.39.0 lets a program put limits on its own work.
within runs a function under a time limit, and optionally a
step or memory budget, and when the limit is reached the work stops at
the line it had reached and you get an Error you can answer
in your own words. actors.stop stops another partition
outright, even one stuck in a loop. A stdio server can write its replies
one JSON line at a time with sys.emit_line. Every
file-system failure now says what kind of failure it was. For game
engines, a stuck actor now faults on its own while the rest keep
running, a frame's actor work can be bounded by messages handled, and
actors run on several cores on Linux too.
#Put a time limit on a piece of work
within(d, f) calls f. If f
finishes in time you get its value as Ok(…), or its own
Result as it is. If it doesn't, the work is stopped and you
get Error({ kind = "timeout", message, at }), where
at is the line it had reached:
fn lookup(name)
return { name = name, stock = 12 }
end
fn forecast(days)
let total = 0
while true do total = total + days end
end
fn run_tool(label, f)
match within(250ms, f)
Ok(result) -> print(label, "->", result)
Error(e) -> if e.kind == "timeout" then
print(label, "-> didn't finish within 250 ms, stopped at line", e.at)
else
print(label, "-> failed:", e.message)
end
end
end
run_tool("lookup", fn() return lookup("rope") end)
run_tool("forecast", fn() return forecast(30) end)
$ scua tools.scua
lookup -> {name = rope, stock = 12}
forecast -> didn't finish within 250 ms, stopped at line 7
The work stops wherever it is, in the interpreter or in compiled
code: in a loop like forecast's, or while waiting on an
ask, a select, a file or a network call, which
all end at the deadline. Files and connections the work opened are
closed. A server that runs tools for an agent can now answer "the tool
didn't finish within 10 s" itself, where before the client got a 503
when the whole request ran out of time. Limits nest, and the tighter one
wins. Under scua test and --fast the deadline
follows the virtual clock, so a test of a timeout gives the same answer
every run.
Give within a table to set budgets as well as a time
limit, any of the three:
fn build_report(n)
let rows = {}
for i in 0:n do rows[i] = `row {i}` end
return len(rows)
end
let limits = { time = 5s, ops = 100000000, memory = 1024 * 1024 }
print(within(limits, fn() return build_report(1000) end))
print(within(limits, fn() return build_report(1000000) end))
$ scua budget.scua
Ok(1000)
Error({kind = memory, message = within: the memory budget ran out, and the work was stopped at line 3, at = 3})
ops counts the steps the work takes and stops it with
kind = "budget". memory counts what it adds
and stops it with kind = "memory".
#Stop a partition, whatever it is doing
actors.drop answers busy for a partition
that is in the middle of a turn. actors.stop(a) ends the
turn at its next check, even in a loop that never waits:
import actors
partition Worker
state n = 0
on Crunch()
while true do n = n + 1 end -- never waits
end
end
partition Supervisor
on Watch(w)
select wait(100ms) -> nil end
print("stopped:", actors.stop(w))
end
end
let w = Worker()
tell w.Crunch()
tell Supervisor().Watch(w)
$ scua stop.scua
stopped: Ok(true)
An ask the partition was serving is answered with
Error({ kind = "stopped" }), the files and connections it
held are closed, and then it is dropped.
#Answer one JSON line at a time
A tool that keeps running and answers request after request, such as
a stdio server speaking JSON lines, writes each reply with
sys.emit_line(value). The line is written and flushed
straight away, and the program carries on:
import sys
while true do
let line = sys.readline()
if line == nil then break end
print("got a request")
sys.emit_line({ echo = line, n = len(line) })
end
$ printf 'alpha\nbeta\n' | scua --allow-tty --output=json server.scua 2>/dev/null
{"echo":"alpha","n":5}
{"echo":"beta","n":4}
Under --output=json these lines are the only thing that
reaches stdout. The two prints went to stderr, so a stray
debugging line in someone's tool code can't corrupt the stream.
Partitions on different cores can call it at once, and their lines never
mix.
#File errors that say what went wrong
Every fs failure now returns
Error({ kind, message }), as fs.write and
fs.lock already did. Code can decide on the
kind and leave the message for people:
import fs
match fs.read_text("settings.toml")
Ok(text) -> print("loaded", len(text), "bytes")
Error(e) -> if e.kind == "not_found" then
print("no settings yet, using the defaults")
else
print(`can't read the settings ({e.kind}): {e.message}`)
end
end
$ scua --allow-fs=data settings.scua
no settings yet, using the defaults
The kinds are not_found, denied,
exists, not_empty, bad_path,
too_large, invalid, unsupported,
closed, timeout and io. A program
can now tell "it isn't there" from "you may not look" in one comparison.
fs.exists uses the same distinction. It answers
true or false when it knows, and an
Error when it can't tell, so a program that starts fresh
when it finds no save won't start fresh over a save it simply wasn't
allowed to see:
import fs
print(fs.exists("settings.toml"))
print(fs.exists("private/save.dat"))
$ scua --allow-fs=data exists.scua
false
Error({kind = denied, message = private/save.dat: permission denied — whether it exists cannot be told})
#For game engines
One actor stuck in a loop no longer stops the others. With an
op_budget set, a partition whose turn uses the whole budget
is stopped by itself and reported to the fault hook with its name and
line. It takes its next message as usual, and every other partition
keeps running.
A frame's actor work can now be bounded by messages handled. Under
scua_run_turns, max_turns counts messages, so
a frame does at most that many handler calls however many partitions are
busy:
scua_turn_budget b = { sizeof b, 64, 2000000 }; // at most 64 messages or 2 ms, whichever comes first
scua_run_turns(p, &b, &report, err, sizeof err);
A long handler is paused and carried on next frame, even when nothing else is running.
The fault hook hears more as well. A handler that ends with an
Error nobody was waiting for arrives as
SCUA_FAULT_ERROR. The partition keeps its state, and the
call's status doesn't change. A text sent to a partition that takes only
the messages it declares is refused with SCUA_ERR_TYPE.
static void on_fault(void *ctx, const scua_fault_info *f) {
if (f->kind == SCUA_FAULT_ERROR) { // a handler gave back an Error; the actor carries on
log_warning(ctx, f->partition, f->partition_len, f->message, f->message_len);
return;
}
mark_actor_broken(ctx, f->actor, f->line);
}
Reusable actor modules work with engine modules now. A partition can
call an engine function through import engine as e, through
a module's own alias for it, or through a record another module returns.
Calls are checked exactly as before.
scua_set_workers(p, n) now runs a script's actors on
several cores on Linux, as it does on macOS and Windows. The workers are
ordinary threads of the game's process. The Linux libscua.a
is built position-independent, so it links into a default (PIE) program
without -no-pie. And a host can free the script text it
passed to scua_load_script as soon as the call returns,
because the loaded script keeps its own copy.
#Upgrading
- Every
fserror is now{ kind, message }. Where you used one as a string, as in`could not read: {e}`orstr.contains(e, …), usee.message. fs.existsreturns anErrorwhen it can't tell. Used straight as anifcondition, thatErrorstops the program with a message, so test for it first withmatches Errorwhere a folder may be unreadable.- Under
scua_run_turnswith a budget,max_turnscounts messages handled, and a handler that runs long is paused at the budget even when nothing else is ready. - An actor that uses its whole
op_budgetis now a fault of that actor alone, reported to the fault hook. The drain carries on, and the call returnsSCUA_ERR_OTHERas it does for any other actor fault, where it used to stop withSCUA_ERR_BUDGET.
The changelog lists everything else in
0.39.0, including http.serve naming the function and value
a request partition couldn't be given, clearer messages for vector maths
on the wrong kinds, and an enum reached through another module printing
by name.