fork-self + waitpid-nonblock + exit-immediate + sleep primitives across c-tier + python-tier
Substrate for fork-per-accept pattern in gpu-worker.lsp — enables async bend dispatch with internal load balancing. c-tier (builtins.c): - bi_fork_self: fork() wrapper, returns 0 in child / pid in parent / #f on fail - bi_waitpid_nonblock: waitpid(-1, WNOHANG), returns reaped pid or 0 - bi_exit_immediate: _exit() wrapper — REQUIRED in fork-self children, regular exit() runs atexit handlers against shared parent state and hangs the child (observed empirically 2026-06-11 via vm-runner.sh). - bi_sleep: real wall-clock sleep(3) — yields CPU. Replaces busy-loop patterns that would (a) burn CPU and (b) SIGKILL in cgroup-limited VMs (observed: 100M iter let-loop SIGKILL'd after 5s in qemu vm). python-tier (lumbda.py): _fork_self / _waitpid_nonblock / _exit_immediate / _sleep mirrors via os.fork / os.waitpid / os._exit / time.sleep. Tested in vm-runner.sh VM (Ubuntu 2G/2vCPU): 3-child fork-cycle test spawns + reaps cleanly 3/3 in both c-tier + python-tier. The exact test pattern that crashed neoblanka host pre-fix now works fine. Asm tier: deferred. Lock retained at chmod a-x ~/git/lumbda/asm/lumbda* per CLAUDE.md threat model.
This commit is contained in:
parent
e57c4948ab
commit
81ac49ece0
2 changed files with 107 additions and 0 deletions
70
c/builtins.c
70
c/builtins.c
|
|
@ -1375,6 +1375,72 @@ static Value bi_spawn_process_stdio(Value *a, int n, Env *e) {
|
||||||
return cons(pin, pout);
|
return cons(pin, pout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* sleep: pause execution for N seconds (integer). Wraps libc sleep(3).
|
||||||
|
* Returns void. Real wall-clock wait — yields CPU.
|
||||||
|
*
|
||||||
|
* Needed for fork-per-accept admission loops (waitpid-nonblock between
|
||||||
|
* sleeps) and any cooperative pacing. Without this, scripts busy-loop
|
||||||
|
* with (let loop () ... (loop)) which (a) burns CPU and (b) hangs in
|
||||||
|
* resource-constrained VMs / fork contexts (SIGKILL after a few sec
|
||||||
|
* under cgroup limits — observed 2026-06-11 vm-runner.sh testing).
|
||||||
|
*/
|
||||||
|
static Value bi_sleep(Value *a, int n, Env *e) {
|
||||||
|
(void)e; CHECK_ARITY("sleep", 1);
|
||||||
|
unsigned int secs = (unsigned int)as_number_int(a[0]);
|
||||||
|
sleep(secs);
|
||||||
|
return VAL_VOID;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* exit-immediate: _exit() wrapper. No atexit handlers, no stdio
|
||||||
|
* flush, no GC cleanup. REQUIRED in fork-self children — calling
|
||||||
|
* regular exit() in a forked child runs libc atexit + stdio
|
||||||
|
* flush against parent's already-shared state, which can hang
|
||||||
|
* or corrupt (seen empirically: minimal `(let ((pid (fork-self)))
|
||||||
|
* (exit 0))` hangs the child until SIGTERM).
|
||||||
|
*
|
||||||
|
* Use (exit-immediate 0) in fork-self child branches. */
|
||||||
|
static Value bi_exit_immediate(Value *a, int n, Env *e) {
|
||||||
|
(void)e;
|
||||||
|
_exit(n > 0 ? (int)as_number_int(a[0]) : 0);
|
||||||
|
return VAL_VOID;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* fork-self: fork() wrapper.
|
||||||
|
*
|
||||||
|
* (fork-self) ⇒ 0 in child, pid in parent, #f on failure
|
||||||
|
*
|
||||||
|
* Used by gpu-worker.lsp to fork-per-accept — parent immediately
|
||||||
|
* returns to accept while child handles the request and exits.
|
||||||
|
* Single-PID-many-handler pattern: parent PID persists, ephemeral
|
||||||
|
* children carry the per-request work + bend-cuda subprocess.
|
||||||
|
*
|
||||||
|
* Caller's responsibility: reap children via (waitpid-nonblock) in
|
||||||
|
* the accept loop, or set SIGCHLD handler. We do NOT install a
|
||||||
|
* default reaper because that would interfere with spawn-process-stdio
|
||||||
|
* which expects synchronous waitpid in handle-request.
|
||||||
|
*/
|
||||||
|
static Value bi_fork_self(Value *a, int n, Env *e) {
|
||||||
|
(void)a; (void)n; (void)e; CHECK_ARITY("fork-self", 0);
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid < 0) return VAL_FALSE;
|
||||||
|
return VAL_INT((int64_t)pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* waitpid-nonblock: reap one completed child (WNOHANG).
|
||||||
|
*
|
||||||
|
* (waitpid-nonblock) ⇒ pid (int) if child reaped, 0 if none ready
|
||||||
|
*
|
||||||
|
* Reaps zombies left by fork-self. Call from accept loop before each
|
||||||
|
* accept to keep zombie count bounded.
|
||||||
|
*/
|
||||||
|
static Value bi_waitpid_nonblock(Value *a, int n, Env *e) {
|
||||||
|
(void)a; (void)n; (void)e; CHECK_ARITY("waitpid-nonblock", 0);
|
||||||
|
int status;
|
||||||
|
pid_t pid = waitpid(-1, &status, WNOHANG);
|
||||||
|
if (pid <= 0) return VAL_INT(0);
|
||||||
|
return VAL_INT((int64_t)pid);
|
||||||
|
}
|
||||||
|
|
||||||
static Value bi_flush_port(Value *a, int n, Env *e) {
|
static Value bi_flush_port(Value *a, int n, Env *e) {
|
||||||
(void)e; CHECK_ARITY("flush-port", 1);
|
(void)e; CHECK_ARITY("flush-port", 1);
|
||||||
ULPort *p = AS_PORT(a[0]);
|
ULPort *p = AS_PORT(a[0]);
|
||||||
|
|
@ -2703,6 +2769,10 @@ Env *make_global_env(void) {
|
||||||
DEF("tcp-send", bi_tcp_send);
|
DEF("tcp-send", bi_tcp_send);
|
||||||
DEF("tcp-close", bi_tcp_close);
|
DEF("tcp-close", bi_tcp_close);
|
||||||
DEF("spawn-process-stdio", bi_spawn_process_stdio);
|
DEF("spawn-process-stdio", bi_spawn_process_stdio);
|
||||||
|
DEF("fork-self", bi_fork_self);
|
||||||
|
DEF("waitpid-nonblock", bi_waitpid_nonblock);
|
||||||
|
DEF("exit-immediate", bi_exit_immediate);
|
||||||
|
DEF("sleep", bi_sleep);
|
||||||
DEF("flush-port", bi_flush_port);
|
DEF("flush-port", bi_flush_port);
|
||||||
DEF("write-binary-file", bi_write_binary_file);
|
DEF("write-binary-file", bi_write_binary_file);
|
||||||
DEF("append-binary-file", bi_append_binary_file);
|
DEF("append-binary-file", bi_append_binary_file);
|
||||||
|
|
|
||||||
37
lumbda.py
37
lumbda.py
|
|
@ -2839,6 +2839,39 @@ def _spawn_process_stdio(path, args):
|
||||||
)
|
)
|
||||||
return Pair(proc.stdin, proc.stdout)
|
return Pair(proc.stdin, proc.stdout)
|
||||||
|
|
||||||
|
import os as _os
|
||||||
|
def _fork_self():
|
||||||
|
"""fork() wrapper. Returns 0 in child, pid in parent, False on
|
||||||
|
failure. Used by gpu-worker.lsp for fork-per-accept pattern —
|
||||||
|
single PID parent, ephemeral children handle requests. Match for
|
||||||
|
lumbda c-tier bi_fork_self (builtins.c)."""
|
||||||
|
try:
|
||||||
|
pid = _os.fork()
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
return pid
|
||||||
|
|
||||||
|
def _waitpid_nonblock():
|
||||||
|
"""waitpid(-1, WNOHANG) wrapper. Returns reaped pid or 0. Match
|
||||||
|
for c-tier bi_waitpid_nonblock."""
|
||||||
|
try:
|
||||||
|
pid, _status = _os.waitpid(-1, _os.WNOHANG)
|
||||||
|
except OSError:
|
||||||
|
return 0
|
||||||
|
return pid
|
||||||
|
|
||||||
|
import time as _time
|
||||||
|
def _sleep(secs):
|
||||||
|
"""Real wall-clock sleep. Match for c-tier bi_sleep."""
|
||||||
|
_time.sleep(int(secs))
|
||||||
|
return VOID
|
||||||
|
|
||||||
|
def _exit_immediate(code):
|
||||||
|
"""os._exit() wrapper. Skip atexit / stdio flush. REQUIRED in
|
||||||
|
fork-self children to avoid dual-cleanup hang. Match for c-tier
|
||||||
|
bi_exit_immediate."""
|
||||||
|
_os._exit(int(code))
|
||||||
|
|
||||||
def _flush_port(port):
|
def _flush_port(port):
|
||||||
"""Flush a write port. No-op if port has no flush method."""
|
"""Flush a write port. No-op if port has no flush method."""
|
||||||
if hasattr(port, 'flush'):
|
if hasattr(port, 'flush'):
|
||||||
|
|
@ -3826,6 +3859,10 @@ def make_global_env():
|
||||||
return out
|
return out
|
||||||
d(S('spawn-process-stdio'),
|
d(S('spawn-process-stdio'),
|
||||||
lambda a, _: _spawn_process_stdio(_str_val(a[0]), _spawn_args(a)))
|
lambda a, _: _spawn_process_stdio(_str_val(a[0]), _spawn_args(a)))
|
||||||
|
d(S('fork-self'), lambda a, _: _fork_self())
|
||||||
|
d(S('waitpid-nonblock'), lambda a, _: _waitpid_nonblock())
|
||||||
|
d(S('exit-immediate'), lambda a, _: _exit_immediate(int(_num(a[0])) if a else 0))
|
||||||
|
d(S('sleep'), lambda a, _: _sleep(int(_num(a[0])) if a else 0))
|
||||||
d(S('flush-port'), lambda a, _: _flush_port(a[0]))
|
d(S('flush-port'), lambda a, _: _flush_port(a[0]))
|
||||||
d(S('write-binary-file'), lambda a, _: _write_binary_file(_str_val(a[0]), _str_val(a[1])))
|
d(S('write-binary-file'), lambda a, _: _write_binary_file(_str_val(a[0]), _str_val(a[1])))
|
||||||
d(S('append-binary-file'), lambda a, _: _append_binary_file(_str_val(a[0]), _str_val(a[1])))
|
d(S('append-binary-file'), lambda a, _: _append_binary_file(_str_val(a[0]), _str_val(a[1])))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue