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:
shoney.arickathil 2026-09-01 22:14:32 +02:00
parent baede5c373
commit c91027bcc6
4 changed files with 251 additions and 0 deletions

View 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 });
}
}
}

View 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"

View file

@ -82,6 +82,13 @@ db-actor:
residency:
./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
# fast path, minutes long: ram+durable x 1/N shards, durability legs,
# gates vs bench/baseline.json. quick = seconds, floors only.

127
scripts/subprocess-accept.sh Executable file
View 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 ]]