feat(lang42): subprocess example + gate
- docs/examples/subprocess: line-oriented TCP service, one Handler actor per request — ping/run/slow/deadline/cap/long exercise the whole bounded surface from .wo, traps caught with try/catch in the language - scripts/subprocess-accept.sh + `just subprocess`: 12 checks, 0 failures first run — deadline and cap messages verbatim, ping answered in 2 ms while a sleep-2 child was parked, SIGTERM exit 0 with the sleep-30 child verifiably gone (pid checked from outside) - service logs to /tmp/subprocess.log, banner-separated per run Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
baede5c373
commit
c91027bcc6
4 changed files with 251 additions and 0 deletions
111
docs/examples/subprocess/main.wo
Normal file
111
docs/examples/subprocess/main.wo
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
-- subprocess — iteration 42's acceptance workload. A line-oriented TCP
|
||||||
|
-- service whose every command exercises the bounded subprocess surface:
|
||||||
|
--
|
||||||
|
-- ping answered without touching proc — the concurrency probe
|
||||||
|
-- run proc.run with the defaults (echo hello)
|
||||||
|
-- slow proc.run of `sleep 2` — the fiber parks, the shard serves
|
||||||
|
-- deadline proc.run_dl of `sleep 10` against 200 ms — caught in .wo
|
||||||
|
-- cap proc.run_dl past a 1000-byte stdout cap — caught in .wo
|
||||||
|
-- long replies, closes, THEN runs `sleep 30`: the SIGTERM drain
|
||||||
|
-- leg's live child (no child survives the stop)
|
||||||
|
--
|
||||||
|
-- One request per connection: read a line, answer a line, close. Every
|
||||||
|
-- request is its own Handler actor, so a parked run blocks nobody — the
|
||||||
|
-- gate proves that by timing `ping` while `slow` is in flight.
|
||||||
|
--
|
||||||
|
-- woc --emit main.wo -o subprocess.wob && wovm subprocess.wob 18971
|
||||||
|
--
|
||||||
|
-- The gate (scripts/subprocess-accept.sh, `just subprocess`) logs the
|
||||||
|
-- service to /tmp/subprocess.log.
|
||||||
|
use env
|
||||||
|
use net
|
||||||
|
use proc
|
||||||
|
|
||||||
|
class ConnMsg {
|
||||||
|
fd: net.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_run() -> Text {
|
||||||
|
let r = try proc.run("echo", ["hello"]) catch (e) nil;
|
||||||
|
if r == nil { return "run-error"; }
|
||||||
|
return "code=${r.code} out=${trim(r.out)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_slow() -> Text {
|
||||||
|
let r = try proc.run("sleep", ["2"]) catch (e) nil;
|
||||||
|
if r == nil { return "slow-error"; }
|
||||||
|
return "slow-done code=${r.code}";
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Traps on purpose: 200 ms deadline against a 10 s sleep. The caller
|
||||||
|
-- catches and reports — catchability from the language is the point.
|
||||||
|
fn deadline_run() -> Text {
|
||||||
|
let r = proc.run_dl("sleep", ["10"], 200, 0, 0);
|
||||||
|
if r == nil { return "nil"; }
|
||||||
|
return "no-trap code=${r.code}";
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Traps on purpose: a chatty child against a 1000-byte stdout cap.
|
||||||
|
fn cap_run() -> Text {
|
||||||
|
let r = proc.run_dl("sh", ["-c", "head -c 99999 /dev/zero"], 5000, 1000, 0);
|
||||||
|
if r == nil { return "nil"; }
|
||||||
|
return "no-trap code=${r.code}";
|
||||||
|
}
|
||||||
|
|
||||||
|
class Handler {
|
||||||
|
pad: Int
|
||||||
|
fn receive(msg: ConnMsg) {
|
||||||
|
let got = try net.read_dl(msg.fd, 256, 5000) catch (e) nil;
|
||||||
|
if got == nil {
|
||||||
|
net.close(msg.fd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let cmd = trim("${got}");
|
||||||
|
print("cmd: ${cmd}");
|
||||||
|
if cmd == "long" {
|
||||||
|
-- answer first: the child must be ALIVE when SIGTERM arrives
|
||||||
|
try net.write(msg.fd, "long-started\n") catch (e) {}
|
||||||
|
net.close(msg.fd);
|
||||||
|
let r = try proc.run("sleep", ["30"]) catch (e) nil;
|
||||||
|
if r == nil { print("long: interrupted"); }
|
||||||
|
else { print("long: code=${r.code}"); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let resp = "unknown";
|
||||||
|
if cmd == "ping" { resp = "pong"; }
|
||||||
|
if cmd == "run" { resp = do_run(); }
|
||||||
|
if cmd == "slow" { resp = do_slow(); }
|
||||||
|
if cmd == "deadline" { resp = try deadline_run() catch (e) "caught: ${e.msg}"; }
|
||||||
|
if cmd == "cap" { resp = try cap_run() catch (e) "caught: ${e.msg}"; }
|
||||||
|
print("resp: ${resp}");
|
||||||
|
try net.write(msg.fd, resp .. "\n") catch (e) {}
|
||||||
|
net.close(msg.fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main(args: multi Text) -> Int {
|
||||||
|
if len(args) < 1 {
|
||||||
|
print_err("usage: subprocess <port>");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
let port = parse_int(args[0]);
|
||||||
|
if port == nil {
|
||||||
|
print_err("subprocess: <port> must be a number");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
let srv = net.listen("127.0.0.1", port);
|
||||||
|
print("listening on 127.0.0.1:${port}");
|
||||||
|
while true {
|
||||||
|
if env.stopping() {
|
||||||
|
-- main's return reaps every parked handler, and each handler's
|
||||||
|
-- live child dies with it (iteration 42's ownership rule)
|
||||||
|
net.close(srv);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let c = net.accept_dl(srv, 250);
|
||||||
|
if c != nil {
|
||||||
|
let h: actor ConnMsg = spawn Handler { pad: 0 };
|
||||||
|
send(h, ConnMsg { fd: c });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
docs/examples/subprocess/wo.toml
Normal file
6
docs/examples/subprocess/wo.toml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
name = "subprocess"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "iteration 42: bounded subprocess — proc.run parked and capped, proc.run_dl per-call bounds, no child survives SIGTERM"
|
||||||
|
|
||||||
|
[runtime]
|
||||||
|
wo = ">= 0.1"
|
||||||
7
justfile
7
justfile
|
|
@ -82,6 +82,13 @@ db-actor:
|
||||||
residency:
|
residency:
|
||||||
./scripts/residency-accept.sh
|
./scripts/residency-accept.sh
|
||||||
|
|
||||||
|
# subprocess: iteration 42's gate (docs/examples/subprocess) — proc.run
|
||||||
|
# from .wo end to end: defaults, deadline and cap traps caught with
|
||||||
|
# try/catch in the language, a parked run blocking no other request, and
|
||||||
|
# the SIGTERM drain leaving no child behind. Log: /tmp/subprocess.log.
|
||||||
|
subprocess:
|
||||||
|
./scripts/subprocess-accept.sh
|
||||||
|
|
||||||
# db-bench: iteration 22's campaign (docs/examples/db-bench) — OFF the
|
# db-bench: iteration 22's campaign (docs/examples/db-bench) — OFF the
|
||||||
# fast path, minutes long: ram+durable x 1/N shards, durability legs,
|
# fast path, minutes long: ram+durable x 1/N shards, durability legs,
|
||||||
# gates vs bench/baseline.json. quick = seconds, floors only.
|
# gates vs bench/baseline.json. quick = seconds, floors only.
|
||||||
|
|
|
||||||
127
scripts/subprocess-accept.sh
Executable file
127
scripts/subprocess-accept.sh
Executable file
|
|
@ -0,0 +1,127 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# scripts/subprocess-accept.sh — iteration 42's gate: bounded subprocess.
|
||||||
|
# The runtime suite (test_proc) proves the mechanics in-process; this
|
||||||
|
# proves the half only a real program shows: proc.run called FROM .wo
|
||||||
|
# through the compiler, bounds caught with try/catch in the language, a
|
||||||
|
# parked run blocking no other request, and the SIGTERM drain leaving no
|
||||||
|
# child behind. Service log: /tmp/subprocess.log (tail -F it live).
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
WOC="$ROOT/compiler/_build/default/bin/woc"
|
||||||
|
WOVM="$ROOT/runtime/wovm"
|
||||||
|
APP="$ROOT/docs/examples/subprocess/main.wo"
|
||||||
|
PORT="${SUBPROCESS_PORT:-18971}"
|
||||||
|
LOG=/tmp/subprocess.log
|
||||||
|
|
||||||
|
pass=0; fail=0
|
||||||
|
ok() { echo "ok $1"; pass=$((pass + 1)); }
|
||||||
|
bad() { echo "FAIL $1"; fail=$((fail + 1)); }
|
||||||
|
|
||||||
|
WORK="$(mktemp -d)"
|
||||||
|
APP_PID=""
|
||||||
|
cleanup() {
|
||||||
|
[[ -n "$APP_PID" ]] && kill -KILL "$APP_PID" 2>/dev/null
|
||||||
|
rm -rf "$WORK"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
now_ms() { echo $(( $(date +%s%N) / 1000000 )); }
|
||||||
|
|
||||||
|
# one request, one line back (the service closes after answering)
|
||||||
|
req() {
|
||||||
|
local line=""
|
||||||
|
if exec 3<>"/dev/tcp/127.0.0.1/$PORT" 2>/dev/null; then
|
||||||
|
printf '%s\n' "$1" >&3
|
||||||
|
IFS= read -r -t "${2:-8}" line <&3 || true
|
||||||
|
exec 3>&- 2>/dev/null
|
||||||
|
fi
|
||||||
|
echo "$line"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- 0. build ------------------------------------------------------------
|
||||||
|
if "$WOC" --emit "$APP" -o "$WORK/subprocess.wob" 2>"$WORK/cerr"; then
|
||||||
|
ok "build: main.wo compiles"
|
||||||
|
else
|
||||||
|
bad "build: $(head -3 "$WORK/cerr")"
|
||||||
|
echo "subprocess-accept: $((pass + fail)) checks, $fail failures"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- 1. start, banner-separated log --------------------------------------
|
||||||
|
{ echo; echo "== subprocess-accept $(date -Is) port $PORT =="; } >>"$LOG"
|
||||||
|
"$WOVM" "$WORK/subprocess.wob" "$PORT" >>"$LOG" 2>&1 &
|
||||||
|
APP_PID=$!
|
||||||
|
up=0
|
||||||
|
for _ in $(seq 1 50); do
|
||||||
|
if (exec 3<>"/dev/tcp/127.0.0.1/$PORT") 2>/dev/null; then exec 3>&-; up=1; break; fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
[[ $up == 1 ]] && ok "service answers on $PORT (log: $LOG)" \
|
||||||
|
|| bad "service never came up (see $LOG)"
|
||||||
|
|
||||||
|
# ---- 2. the default run --------------------------------------------------
|
||||||
|
got="$(req run)"
|
||||||
|
[[ "$got" == "code=0 out=hello" ]] && ok "proc.run: code=0 out=hello" \
|
||||||
|
|| bad "proc.run answered '$got'"
|
||||||
|
|
||||||
|
# ---- 3. deadline caught IN the language ----------------------------------
|
||||||
|
got="$(req deadline)"
|
||||||
|
if [[ "$got" == caught:*deadline* ]]; then
|
||||||
|
ok "deadline trap caught in .wo: '$got'"
|
||||||
|
else
|
||||||
|
bad "deadline leg answered '$got'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- 4. stdout cap caught IN the language --------------------------------
|
||||||
|
got="$(req cap)"
|
||||||
|
if [[ "$got" == caught:*"stdout cap 1000"* ]]; then
|
||||||
|
ok "cap trap caught in .wo: '$got'"
|
||||||
|
else
|
||||||
|
bad "cap leg answered '$got'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- 5. a parked run blocks nobody ---------------------------------------
|
||||||
|
slow_out="$WORK/slow"
|
||||||
|
( req slow 10 >"$slow_out" ) &
|
||||||
|
SLOW_JOB=$!
|
||||||
|
sleep 0.4 # the slow child (sleep 2) is now in flight
|
||||||
|
t0=$(now_ms)
|
||||||
|
got="$(req ping)"
|
||||||
|
t1=$(now_ms)
|
||||||
|
[[ "$got" == "pong" ]] && ok "ping answered while slow runs" \
|
||||||
|
|| bad "ping answered '$got' while slow runs"
|
||||||
|
(( t1 - t0 < 1500 )) && ok "ping took $((t1 - t0)) ms (not slow's 2 s)" \
|
||||||
|
|| bad "ping took $((t1 - t0)) ms — the shard was blocked"
|
||||||
|
wait "$SLOW_JOB"
|
||||||
|
got="$(cat "$slow_out")"
|
||||||
|
[[ "$got" == "slow-done code=0" ]] && ok "slow completed: '$got'" \
|
||||||
|
|| bad "slow answered '$got'"
|
||||||
|
|
||||||
|
# ---- 6. SIGTERM drain: no child survives ---------------------------------
|
||||||
|
got="$(req long)"
|
||||||
|
[[ "$got" == "long-started" ]] && ok "long child started" \
|
||||||
|
|| bad "long answered '$got'"
|
||||||
|
CHILD=""
|
||||||
|
for _ in $(seq 1 30); do
|
||||||
|
CHILD="$(pgrep -P "$APP_PID" -x sleep | head -1 || true)"
|
||||||
|
[[ -n "$CHILD" ]] && break
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
[[ -n "$CHILD" ]] && ok "live child found (sleep 30, pid $CHILD)" \
|
||||||
|
|| bad "no sleep child appeared under the service"
|
||||||
|
kill -TERM "$APP_PID"
|
||||||
|
rc=-1
|
||||||
|
for _ in $(seq 1 50); do
|
||||||
|
if ! kill -0 "$APP_PID" 2>/dev/null; then wait "$APP_PID"; rc=$?; break; fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
[[ $rc == 0 ]] && ok "SIGTERM: clean exit 0" || bad "SIGTERM exit was $rc"
|
||||||
|
if [[ -n "$CHILD" ]]; then
|
||||||
|
kill -0 "$CHILD" 2>/dev/null && bad "child $CHILD SURVIVED the stop" \
|
||||||
|
|| ok "child $CHILD is gone with the service"
|
||||||
|
fi
|
||||||
|
APP_PID=""
|
||||||
|
|
||||||
|
echo "subprocess-accept: $((pass + fail)) checks, $fail failures"
|
||||||
|
[[ $fail == 0 ]]
|
||||||
Loading…
Reference in a new issue