A running actor can now be saved to bytes from a script and brought
back later, with its state intact and the same identity it had before.
This release also lets a web server share your program with background
tasks and actors, adds scua serve for serving a folder in
one command, makes long-running work take turns wherever it runs, and
gives data tables four new verbs. A few changes are worth reading before
you upgrade.
#Actors can now be frozen and revived from a script
Each partition keeps its whole state in its own isolated heap, so a
running actor can be turned into bytes and turned back into a running
actor. The actors module has three functions for it:
import actors
partition Counter
state n = 0
on Add(k)
n = n + k
print(`count is {n}`)
end
end
fn park(c)
let blob = actors.freeze(c)? -- the bytes
actors.drop(c)? -- stop it and free its memory
return actors.revive(blob) -- the same actor, back again
end
let c = Counter()
tell c.Add(2)
tell c.Add(3)
wait(0ms)
match park(c)
Ok(back) -> tell back.Add(10)
Error(e) -> print(`could not park: {e}`)
end
count is 2
count is 5
count is 15
freeze takes a snapshot and leaves the actor running,
which is why drop exists alongside it. The pair lets a
long-running server keep only its busy sessions in memory: freeze the
idle ones, drop them, and revive one when its next request arrives. A
handler that was paused halfway through a turn, waiting on a reply,
comes back paused at the same point.
revive returns the same actor. Its identity travels
inside the bytes, so a reference you handed out before the freeze still
reaches it afterwards. actors.generation(blob) gives a
number that goes up with every freeze and keeps going up across a
revive. Store it next to the bytes and refuse a write whose number is
lower, and a process that woke an older copy cannot overwrite newer
state.
None of the three needs a capability. Moving the bytes anywhere still
does: write them with fs, or send them with
net.
#A server now runs alongside the rest of your program
http.serve shares one loop with everything else in the
program. A task you spawn before the call keeps running,
and a handler can send a message to an actor that is handled while the
server carries on:
import http
partition Audit
state seen = 0
on Hit(path)
seen = seen + 1
print(`audit: request {seen} for {path}`)
end
end
let audit = Audit()
fn handle(req)
tell audit.Hit(req.path)
return { status = 200, body = b"ok\n" }
end
http.serve(handle, { bind = "127.0.0.1:8080" })
Two requests later, the actor has logged both:
audit: request 1 for /a
audit: request 2 for /b
Put background work before the http.serve line, because
that call does not come back. A handler runs to completion: it can
spawn work to finish after it has answered, and it can
tell an actor, but it cannot wait or
ask. One that tries gets a 500 that says why.
Request handlers are compiled by the same optimizing tier as the rest
of your code, so a handler that loops over data runs at the speed that
loop runs anywhere else. --serve-timeout stops a runaway
handler at its deadline whether or not it has been compiled, and it
stops only that handler.
#scua serve starts a web server in one command
Run scua serve in a folder and it serves that folder
over HTTP:
$ scua serve
scua: serving . on http://127.0.0.1:8000/ (Ctrl-C to stop)
If the port is taken, it moves to the next free one and tells you which port it skipped:
$ scua serve
scua: port 8000 was busy — serving . on http://127.0.0.1:8001/ (Ctrl-C to stop)
It is for previewing a page or a folder of assets locally. It serves files and nothing else, and by default it listens on your own machine only.
#Long-running work now takes turns
The runtime pauses a long-running task every so often so that other work gets a go, and that now happens everywhere work runs: in compiled code as well as interpreted code, in actor message handlers, and in tasks spawned inside a partition. A short task started before a long loop runs while the loop is still going:
spawn(fn() print("the short task ran") end)
let total = 0
for i in 0:50000000 do total = total + i end
print("the long loop finished:", total)
the short task ran
the long loop finished: 1249999975000000
The same applies between partitions, so a partition with a long
handler shares the worker with the others. A handler that is paused
picks up exactly where it stopped, and a partition still handles one
message at a time. --max-ops counts each task on its own,
so a limit you set applies to a task however often it is paused, and one
task's work never uses up another's allowance. A sandboxed partition's
limit covers the whole message, pauses included.
#Data tables gained four verbs
dropna removes rows with gaps, either anywhere in the
row or in one named column. fillna fills the gaps in a
column, and only with a value that fits it exactly, so an exact money
column stays exact. cumsum adds a running total as a new
column, and corr measures how strongly two numeric columns
move together. group_by now takes several keys, and each
key comes back as its own column:
let sales = frame({
region = ["north", "south", "north", "north", "south"],
quarter = ["Q1", "Q1", "Q2", "Q1", "Q2"],
amount = [100.00d, 50.00d, 200.00d, nil, 75.00d],
})
print(sales.dropna("amount").group_by(["region", "quarter"], { revenue = "sum:amount", orders = "count" }))
print(sales.dropna("amount").cumsum("amount", "running"))
region quarter revenue orders
north Q1 100.00 1
south Q1 50.00 1
north Q2 200.00 1
south Q2 75.00 1
(4 rows × 4 cols)
region quarter amount running
north Q1 100.00 100.00
south Q1 50.00 150.00
north Q2 200.00 350.00
south Q2 75.00 425.00
(4 rows × 4 cols)
A printed table now shows six meaningful digits, so a
describe() mean reads as 135.958. The values
themselves are unchanged, exact columns always print in full, and
to_csv() still writes every digit.
#Upgrading
Three changes can make a program that ran before report an error. In each case the program was not doing what it appeared to.
- Sending a message a partition has no handler for is now an error.
tell box.Adld(1)against a partition that handlesAddused to do nothing. It is now a compile error where the compiler can see which partitionboxholds, and a runtime error on the same line where it cannot, and both name the handler you probably meant. ask,wait_forandselectnow report an error when called outside an actor handler. They used to returnnil, solet answer = ask thing.Question()at the top level setanswerto nothing. The error says what to use instead.- Running with
--fastwhile a server is listening is refused.--fastskips through waits, and a network socket is the one thing it cannot skip.
Two smaller things: a partition that is serving cannot be saved (the
attempt names the listener), and wait now works at the top
level of a scua test file.
#What's next
Partitions share nothing and talk only by message, so they can run in parallel. Running them across every core of the machine is the next step.
The full changelog has the rest.