feat(examples): fibers — the hybrid scheduler demonstrated + gate
- docs/examples/fibers: part 1 is TIMING-FREE and byte-exact — main sends three messages then burns reductions; each budget expiry hands the Counter actor exactly one delivery (cooperative mechanics, preemptive fairness, BEAM's shape); part 2 parks a Sleeper actor mid-receive on the I/O plane while main keeps ticking — the wake lands between ticks, proving a sleeping fiber blocks nobody - the missing "sleeper: up" on the first run was main-return-reap working as specced (main ended before the deadline); the demo's window widened so the wake is observable - scripts/fibers-accept.sh + `just fibers` (8 checks): build, part-1 exact + part-2 ordering invariants on auto/uring/epoll backends, and an ASan-runtime rebuild+run - README points at the doctrine writeup (exploration/fibers) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
503b19ac4b
commit
efbf2f36ca
6 changed files with 184 additions and 0 deletions
3
docs/examples/fibers/.gitignore
vendored
Normal file
3
docs/examples/fibers/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
|
||||
# built artifacts
|
||||
target/
|
||||
21
docs/examples/fibers/README.md
Normal file
21
docs/examples/fibers/README.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# fibers — the hybrid scheduler, demonstrated
|
||||
|
||||
The smallest program that shows all three legs of the concurrency model
|
||||
(the doctrine-depth writeup lives in
|
||||
[`docs/plan/exploration/fibers/00-fibers.md`](../../plan/exploration/fibers/00-fibers.md)):
|
||||
|
||||
1. **Reduction-budget eviction** — part 1's output is byte-exact and
|
||||
timing-free: main sends three messages, then burns reductions; every
|
||||
time its budget expires the counter actor gets a turn and delivers
|
||||
exactly one message. Cooperative mechanics, preemptive fairness.
|
||||
2. **Actors as the spawn surface** — `spawn Counter { ... }` returns an
|
||||
`actor Tick`; `send` moves the message (using it afterwards is a
|
||||
compile error); delivery is one message at a time per actor.
|
||||
3. **Parking, not blocking** — part 2's sleeper actor calls `time.sleep`
|
||||
mid-receive: the fiber parks on the shard's I/O plane (io_uring
|
||||
primary, epoll fallback — `WO_IO=uring|epoll` forces either) and
|
||||
main keeps ticking while it sleeps.
|
||||
|
||||
Run: `just fibers` (the acceptance gate builds it, checks part 1
|
||||
byte-exact, asserts part 2's ordering invariants, and repeats the run on
|
||||
both I/O backends plus ASan).
|
||||
74
docs/examples/fibers/main.wo
Normal file
74
docs/examples/fibers/main.wo
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
use time
|
||||
|
||||
-- fibers — the writeonce hybrid demonstrated (BEAM's shape):
|
||||
-- cooperative MECHANICS (no signals, no interrupts) with
|
||||
-- reduction-budget EVICTION (loop back-edges pay one reduction; at
|
||||
-- zero the fiber re-queues whether it likes it or not), actors as the
|
||||
-- spawn surface (one message at a time, ownership-moving sends), and
|
||||
-- blocking builtins that PARK on the shard's io_uring plane instead
|
||||
-- of holding the thread.
|
||||
--
|
||||
-- Part 1 is deterministic by construction (budget accounting, no time):
|
||||
-- its output is byte-exact. Part 2 shows a sleeping fiber not blocking
|
||||
-- anyone; its ordering is asserted loosely by the acceptance gate
|
||||
-- because it depends on real time.
|
||||
|
||||
class Tick {
|
||||
n: Int
|
||||
}
|
||||
|
||||
-- Part 1: a counting actor; main and the actor interleave one message
|
||||
-- per main-yield under the default budget.
|
||||
class Counter {
|
||||
label: Text
|
||||
total: Int
|
||||
fn receive(msg: Tick) {
|
||||
self.total = self.total + msg.n;
|
||||
print("${self.label} +${msg.n} = ${self.total}");
|
||||
}
|
||||
}
|
||||
|
||||
-- Part 2: an actor whose receive PARKS mid-message. The park releases
|
||||
-- the shard: main keeps ticking while this fiber sleeps.
|
||||
class Sleeper {
|
||||
pad: Int
|
||||
fn receive(msg: Tick) {
|
||||
print("sleeper: down for ${msg.n}ms");
|
||||
time.sleep(msg.n);
|
||||
print("sleeper: up");
|
||||
}
|
||||
}
|
||||
|
||||
fn spin(rounds: Int) {
|
||||
-- burn reductions so the scheduler's eviction gets a chance: each
|
||||
-- back-edge pays one reduction; the default budget is 4000
|
||||
let i = 0;
|
||||
while i < rounds {
|
||||
i = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Int {
|
||||
-- ---- part 1: deterministic budget interleave ----
|
||||
let c: actor Tick = spawn Counter { label: "count", total: 0 };
|
||||
send(c, Tick { n: 1 });
|
||||
send(c, Tick { n: 2 });
|
||||
send(c, Tick { n: 3 });
|
||||
-- three yields deliver exactly three messages, one per turn
|
||||
spin(5000);
|
||||
spin(5000);
|
||||
spin(5000);
|
||||
print("part1 done");
|
||||
|
||||
-- ---- part 2: a parked fiber blocks nobody ----
|
||||
let s: actor Tick = spawn Sleeper { pad: 0 };
|
||||
send(s, Tick { n: 150 });
|
||||
let t = 0;
|
||||
while t < 8 {
|
||||
time.sleep(25);
|
||||
print("main tick ${t}");
|
||||
t = t + 1;
|
||||
}
|
||||
print("part2 done");
|
||||
return 0;
|
||||
}
|
||||
6
docs/examples/fibers/wo.toml
Normal file
6
docs/examples/fibers/wo.toml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
name = "fibers"
|
||||
version = "0.1.0"
|
||||
description = "the hybrid scheduler demonstrated: budget eviction + actors + parked sleeps"
|
||||
|
||||
[runtime]
|
||||
wo = ">= 0.1"
|
||||
6
justfile
6
justfile
|
|
@ -43,6 +43,12 @@ deps-accept:
|
|||
web-app:
|
||||
./scripts/web-app-accept.sh
|
||||
|
||||
# fibers: the hybrid-scheduler demo (docs/examples/fibers) — part 1 byte-
|
||||
# exact budget interleave, part 2 parked-sleeper-blocks-nobody, on the
|
||||
# uring AND epoll backends plus an ASan run.
|
||||
fibers:
|
||||
./scripts/fibers-accept.sh
|
||||
|
||||
# install-accept: extract the dist tarball to a temp prefix, PATH it, and prove
|
||||
# `woc version` + a from-scratch project build+run (self-located wovm) + the
|
||||
# wo-constraint refusal all work — the "tarball install actually works" gate.
|
||||
|
|
|
|||
74
scripts/fibers-accept.sh
Executable file
74
scripts/fibers-accept.sh
Executable file
|
|
@ -0,0 +1,74 @@
|
|||
#!/usr/bin/env bash
|
||||
# scripts/fibers-accept.sh — the hybrid-scheduler demo's gate: part 1 is
|
||||
# byte-exact (budget accounting, timing-free); part 2 asserts ordering
|
||||
# invariants (a parked sleeper blocks nobody) on BOTH I/O backends and
|
||||
# under ASan.
|
||||
set -uo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
WOC="$ROOT/compiler/_build/default/bin/woc"
|
||||
WOVM="$ROOT/runtime/wovm"
|
||||
DIR="$ROOT/docs/examples/fibers"
|
||||
|
||||
pass=0; fail=0
|
||||
ok() { echo "ok $1"; pass=$((pass + 1)); }
|
||||
bad() { echo "FAIL $1 -- $2"; fail=$((fail + 1)); }
|
||||
|
||||
if [[ ! -x "$WOC" || ! -x "$WOVM" ]]; then
|
||||
echo "fibers-accept: build woc and wovm first" >&2; exit 1
|
||||
fi
|
||||
|
||||
if WO_RUNTIME="$WOVM" "$WOC" "$DIR" >/dev/null 2>&1 && [[ -x "$DIR/target/fibers" ]]; then
|
||||
ok "builds"
|
||||
else
|
||||
bad "build" "woc failed"; echo "fibers-accept: 1 checks, 1 failures"; exit 1
|
||||
fi
|
||||
|
||||
check_run() { # name [env pairs...]
|
||||
local name="$1"; shift
|
||||
local out
|
||||
out="$(env "$@" "$DIR/target/fibers" 2>&1)"
|
||||
local want_p1=$'count +1 = 1\ncount +2 = 3\ncount +3 = 6\npart1 done'
|
||||
if [[ "$(printf '%s\n' "$out" | head -4)" == "$want_p1" ]]; then
|
||||
ok "$name: part 1 byte-exact (budget interleave)"
|
||||
else
|
||||
bad "$name part1" "$(printf '%s' "$out" | head -4 | tr '\n' '|')"
|
||||
fi
|
||||
local down_ln up_ln done_ln ticks
|
||||
down_ln=$(printf '%s\n' "$out" | grep -n "sleeper: down" | cut -d: -f1)
|
||||
up_ln=$(printf '%s\n' "$out" | grep -n "sleeper: up" | cut -d: -f1)
|
||||
done_ln=$(printf '%s\n' "$out" | grep -n "part2 done" | cut -d: -f1)
|
||||
ticks=$(printf '%s\n' "$out" | grep -c "main tick")
|
||||
if [[ -n "$down_ln" && -n "$up_ln" && -n "$done_ln" && "$ticks" == "8" ]]; then
|
||||
local before
|
||||
before=$(printf '%s\n' "$out" | sed -n "${down_ln},${up_ln}p" | grep -c "main tick")
|
||||
if [[ "$before" -ge 3 && "$up_ln" -lt "$done_ln" ]]; then
|
||||
ok "$name: part 2 — parked sleeper blocked nobody ($before ticks while down)"
|
||||
else
|
||||
bad "$name part2" "only $before ticks before wake"
|
||||
fi
|
||||
else
|
||||
bad "$name part2" "down=$down_ln up=$up_ln done=$done_ln ticks=$ticks"
|
||||
fi
|
||||
}
|
||||
|
||||
check_run "auto"
|
||||
check_run "uring" WO_IO=uring
|
||||
check_run "epoll" WO_IO=epoll
|
||||
|
||||
# ASan flavor: rebuild the binary against the ASan runtime and repeat once
|
||||
make -C "$ROOT/runtime" wovm-asan -s >/dev/null 2>&1
|
||||
if "$WOC" build "$DIR" -o "$DIR/target/fibers_asan" --runtime "$ROOT/runtime/build/wovm_asan" >/dev/null 2>&1; then
|
||||
out="$("$DIR/target/fibers_asan" 2>&1)"
|
||||
if printf '%s' "$out" | grep -q "part2 done" && ! printf '%s' "$out" | grep -qi "sanitizer\|leak"; then
|
||||
ok "ASan run clean"
|
||||
else
|
||||
bad "asan" "$(printf '%s' "$out" | tail -2 | tr '\n' '|')"
|
||||
fi
|
||||
else
|
||||
bad "asan" "build failed"
|
||||
fi
|
||||
|
||||
echo
|
||||
printf 'fibers-accept: %d checks, %d failures\n' "$((pass + fail))" "$fail"
|
||||
[[ $fail -eq 0 ]]
|
||||
Loading…
Reference in a new issue