From c91027bcc6257eaeff3f16b49e30c8c1f0b04fca Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Tue, 1 Sep 2026 22:14:32 +0200 Subject: [PATCH] feat(lang42): subprocess example + gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- docs/examples/subprocess/main.wo | 111 +++++++++++++++++++++++++++ docs/examples/subprocess/wo.toml | 6 ++ justfile | 7 ++ scripts/subprocess-accept.sh | 127 +++++++++++++++++++++++++++++++ 4 files changed, 251 insertions(+) create mode 100644 docs/examples/subprocess/main.wo create mode 100644 docs/examples/subprocess/wo.toml create mode 100755 scripts/subprocess-accept.sh diff --git a/docs/examples/subprocess/main.wo b/docs/examples/subprocess/main.wo new file mode 100644 index 0000000..3aefb0e --- /dev/null +++ b/docs/examples/subprocess/main.wo @@ -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 "); + return 2; + } + let port = parse_int(args[0]); + if port == nil { + print_err("subprocess: 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 }); + } + } +} diff --git a/docs/examples/subprocess/wo.toml b/docs/examples/subprocess/wo.toml new file mode 100644 index 0000000..565d275 --- /dev/null +++ b/docs/examples/subprocess/wo.toml @@ -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" diff --git a/justfile b/justfile index 45b2f7c..dd12490 100644 --- a/justfile +++ b/justfile @@ -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. diff --git a/scripts/subprocess-accept.sh b/scripts/subprocess-accept.sh new file mode 100755 index 0000000..6a098e6 --- /dev/null +++ b/scripts/subprocess-accept.sh @@ -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 ]]