code wiki / _hdl_build / nx_supervised_dispatch.nx
nx_supervised_dispatch.nx source
↩ module page · 305 lines · 16625 B
1// nx_supervised_dispatch.nx -- WMS rung M5: OS / KERNEL SUPERVISOR BINDING.
2//
3// module: nishi-core.autonomy.supervised_dispatch
4// capability: BIND_DISPATCH_TO_SUPERVISED_JOB (a dispatched rung becomes a real,
5// resource-LIMITED, reaped, RESTART-on-death supervised job)
6//
7// WHAT THIS UNIFIES (the M5 payoff): M4 (nx_dispatch_lease) LEASES the next ready rung
8// but never RUNS it; M1 (nx_heartbeat_monitor) detects a stalled stream but never ACTS;
9// the supervisor policy (nx_supervisor) decides RESTART/ESCALATE but never forks. M5
10// is the missing seam: it FORKS the leased rung as a governed child (with a kernel
11// resource limit = prlimit64 RLIMIT_AS, the Linux analog of a Job-Object memory limit),
12// beats its heartbeat, and -- when M1 sees a missing heartbeat (a crashed job) -- it
13// RESTARTS/reassigns (a second supervised run). Conductor loop + kernel scheduler +
14// workstream manager collapse into ONE control plane.
15//
16// REUSE / lineage (compose, do not reinvent -- check-registries-before-building):
17// - dl_dispatch / dl_try_lease / dl_release <- nx_dispatch_lease.nx (M4): lease the rung
18// - an_newcx / an_load / an_find / an_id_at <- nx_assign_core (surfaced via M4)
19// - hb_beat_at / hbm_scan / HB_STALLED/HB_ALIVE <- nx_heartbeat_monitor.nx (M1)
20// - fa_cat / fa_catn / fa_appendz <- nx_framed_append.nx (R0b): the locked
21// single-write status discipline (every record = ONE buffer -> ONE atomic write)
22// - the fork -> child-setup -> sys_wait4 status-decode model is the nx_container.nx idiom
23// (lo = status & 0xFF; exited iff lo==0; code = (status>>8)&0xFF) -- reimplemented HERE
24// (NOT imported) because nx_container.nx imports nx_syscalls_x86_64.nx and that would
25// surface a SECOND syscall table against M1/M4's nx_syscalls.nx = the double-import
26// rc=6 trap. M5 keeps a SINGLE syscall surface (nx_syscalls.nx via all three imports).
27//
28// SOVEREIGN PAYLOAD (no /bin dependency): the supervised child does NOT execve a foreign
29// ELF. After fork it caps ITSELF (nx_prlimit) then runs an IN-ORGAN payload selector --
30// the same organ post-fork executing a payload function (the nx_container fork model minus
31// execve). payload=0 clean work+exit(0); payload=1 "crash" exit(70); payload=2 hang
32// (finite busy then exit WITHOUT beating = the never-beats stall the gate KILLs/detects).
33//
34// SUPERVISOR POLICY (inlined, ~ nx_supervisor's 4-state restart-budget machine): a STALLED
35// job under budget -> RESTART (respawn + restarts++ + fresh beat); over budget -> ESCALATE
36// (bounded, no thrash storm); an ALIVE / clean-exited job -> NONE (the no-thrash neg-ctrl).
37//
38// WRITE DISCIPLINE (the torn-line bug we must NOT reintroduce): every status record is
39// assembled into ONE buffer and emitted with a SINGLE locked fa_appendz (the cn_emit_*
40// discipline) -- never a sequence of sys_write() calls. Additive: new file only; one tiny
41// additive nx_prlimit wrapper was added to nx_syscalls.nx (no existing function touched).
42// license_tier: ORIGINAL
43import "nx_dispatch_lease.nx" // dl_dispatch / dl_try_lease / dl_release (+ an_*, nx_syscalls, framed_append)
44import "nx_heartbeat_monitor.nx" // hb_beat_at / hbm_scan / HB_ALIVE / HB_STALLED (+ nx_ws_ledger)
45import "nx_framed_append.nx" // fa_cat / fa_catn / fa_appendz (status discipline)
46const SD_MAGIC_2277: i64 = 2277
47const SD_MAGIC_100000: i64 = 100000
48
49const SD_REC_CAP: i64 = 256 // bounded status record size (no magic number)
50const SD_SIGKILL: i64 = 9 // the crash signal for the controlled-kill test
51const SD_MAXRESTART: i64 = 3 // restart budget; over -> ESCALATE (no thrash storm)
52const SD_AS_LIMIT: i64 = 0x10000000 // 256 MiB address-space cap = the resource "limit"
53
54// supervisor actions (mirror nx_supervisor's NX_SUP_ACTION_* without importing it)
55const SD_ACTION_NONE: i64 = 0 // alive / clean-exited -> do nothing (no-thrash path)
56const SD_ACTION_RESTART: i64 = 1 // stalled + under budget -> respawn
57const SD_ACTION_ESCALATE: i64 = 3 // stalled + budget exhausted -> escalate (bounded)
58
59// job state lanes
60const SD_RUNNING: i64 = 0
61const SD_EXITED_OK: i64 = 1
62const SD_CRASHED: i64 = 2
63const SD_ESCALATED: i64 = 3
64
65// payload selectors (the in-organ supervised work; no /bin, no execve)
66const SD_PAY_CLEAN: i64 = 0 // compute + exit(0): a job that finishes normally
67const SD_PAY_CRASH: i64 = 1 // exit(70): a job that dies non-zero
68const SD_PAY_HANG: i64 = 2 // finite busy, exit WITHOUT beating (the stall payload)
69
70// SdJob descriptor as a flat i64 block (8 slots, no heap churn). Layout:
71// j[0]=ws_id j[1]=pid j[2]=restarts j[3]=state j[4]=limit_as j[5]=last_exit/sig
72const SD_J_WS: i64 = 0
73const SD_J_PID: i64 = 1
74const SD_J_RESTARTS: i64 = 2
75const SD_J_STATE: i64 = 3
76const SD_J_LIMIT: i64 = 4
77const SD_J_EXIT: i64 = 5
78
79func sd_job_new() -> *i64 {
80 let j: *i64 = sys_mmap(8 * 8) as *i64
81 j[SD_J_WS] = 0; j[SD_J_PID] = 0; j[SD_J_RESTARTS] = 0
82 j[SD_J_STATE] = SD_RUNNING; j[SD_J_LIMIT] = SD_AS_LIMIT; j[SD_J_EXIT] = 0
83 return j
84}
85
86func sd_getpid() -> i64 { return __syscall(172, 0, 0, 0, 0, 0, 0) } // rv64 getpid=172; raw x86 39 is an RV64 KEY translated to ioctl(16) -> -ENOTTY (debt idx SD_MAGIC_2277)
87
88// Apply a kernel resource limit (RLIMIT_AS) to SELF (pid=0). Called in the forked child
89// BEFORE the payload runs -> the supervised job is genuinely resource-LIMITED (the Job-
90// Object-with-a-limit). Reads the value back (old_limit) and returns the ENFORCED
91// rlim_cur so a caller/child can prove the cap actually stuck (T2). Returns -errno on
92// failure (e.g. EPERM in a locked-down sandbox -> the gate honestly reports RED, no fake).
93func sd_apply_limit(limit_as: i64) -> i64 {
94 let nl: *u8 = sys_mmap(16) // struct rlimit64 { cur, max }
95 let nlp: *i64 = nl as *i64
96 nlp[0] = limit_as // rlim_cur
97 nlp[1] = limit_as // rlim_max (soft==hard; lowering is unprivileged-OK)
98 // SET (old_limit=0: we do NOT want the previous value -- prlimit writes the OLD limit into
99 // old_limit, and the old RLIMIT_AS is RLIM_INFINITY=-1, which would falsely look like a
100 // failure). rc<0 => the cap was DENIED (e.g. EPERM in a locked sandbox) -> propagate.
101 let rc: i64 = nx_prlimit(0, RLIMIT_AS, nl, 0 as *u8)
102 if rc < 0 { return rc }
103 // READ BACK fresh (new=0, old=&cur): returns the NOW-ENFORCED rlim_cur -> the proof the
104 // cap actually stuck (the gate's T2 child asserts this == SD_AS_LIMIT).
105 let cur: *u8 = sys_mmap(16)
106 let rcr: i64 = nx_prlimit(0, RLIMIT_AS, 0 as *u8, cur)
107 if rcr < 0 { return rcr }
108 let curp: *i64 = cur as *i64
109 return curp[0] // the enforced rlim_cur (== limit_as on success)
110}
111
112// The sovereign supervised PAYLOAD (runs ONLY in the forked child; never returns).
113// beats: if 1 AND payload != HANG, the child emits ONE heartbeat for ws (proves a live
114// supervised job beats). The HANG payload never beats by design (the stall).
115func sd_child_payload(hbpath: *u8, ws: i64, payload: i64, limit_as: i64, beats: i64,
116 beat_epoch: i64) -> i64 {
117 let enforced: i64 = sd_apply_limit(limit_as) // cap SELF first (the resource limit)
118 if payload == SD_PAY_HANG {
119 // hang: a finite busy spin (sovereign, bounded -- the gate KILLs it before this
120 // ends, but it self-terminates if the kill is ever missed so no zombie wedge).
121 sys_sleep_ms(SD_MAGIC_100000) // ~100s; the gate's nx_kill arrives first
122 sys_exit(80) // (only reached if never killed)
123 return 80
124 }
125 if beats == 1 {
126 let pid: i64 = sd_getpid()
127 hb_beat_at(hbpath, ws, beat_epoch, 0, pid) // ONE locked beat = "this job is alive"
128 }
129 if payload == SD_PAY_CRASH {
130 sys_exit(70) // a job that dies non-zero
131 return 70
132 }
133 // SD_PAY_CLEAN: a small deterministic compute, then a normal exit(0).
134 var acc: i64 = 0
135 var i: i64 = 0
136 while i < 1000 { acc = acc + i; i = i + 1 }
137 if enforced < 0 { sys_exit(91) } // limit failed -> distinct non-zero
138 sys_exit(0) // finished normally (no restart wanted)
139 return 0
140}
141
142// FORK a supervised job: child caps itself + runs payload(ws); parent stamps pid +
143// RUNNING and returns the child pid (>0). The parent ALSO beats an initial heartbeat
144// for ws (beat_epoch) so a freshly-spawned job is immediately ALIVE to the M1 scan even
145// before the child's own beat lands (race-free liveness). Returns child pid, or -errno.
146func sd_spawn_supervised(hbpath: *u8, job: *i64, payload: i64, beat_epoch: i64) -> i64 {
147 let pid: i64 = sys_fork()
148 if pid < 0 { return pid } // fork failed -> -errno
149 if pid == 0 {
150 // CHILD: a clean job/crash beats; a hang never beats (gate detects the stall).
151 var beats: i64 = 1
152 if payload == SD_PAY_HANG { beats = 0 }
153 sd_child_payload(hbpath, job[SD_J_WS], payload, job[SD_J_LIMIT], beats, beat_epoch)
154 sys_exit(73) // unreachable safety net
155 return 73
156 }
157 // PARENT: record + an initial supervisor-side beat so the job is ALIVE at once.
158 job[SD_J_PID] = pid
159 job[SD_J_STATE] = SD_RUNNING
160 hb_beat_at(hbpath, job[SD_J_WS], beat_epoch, 0, sd_getpid())
161 return pid
162}
163
164// NON-BLOCKING liveness poll of one job via wait4(pid, &st, WNOHANG).
165// returns 0 = still running (not reaped)
166// 1 = exited OK (code 0) -> job.state = EXITED_OK
167// 2 = crashed (nonzero exit OR killed by signal) -> job.state = CRASHED
168// Decodes status with the nx_container idiom (lo = status & 0xFF; exited iff lo==0).
169func sd_poll(job: *i64) -> i64 {
170 let stp: *i64 = sys_mmap(16) as *i64
171 stp[0] = 0
172 let r: i64 = sys_wait4(job[SD_J_PID], stp, WNOHANG)
173 if r == 0 { return 0 } // WNOHANG: child not yet reapable = running
174 if r < 0 { return 0 } // no such child to reap right now
175 let status: i64 = stp[0]
176 let lo: i64 = status & 0xFF
177 if lo == 0 {
178 let code: i64 = (status >> 8) & 0xFF
179 job[SD_J_EXIT] = code
180 if code == 0 { job[SD_J_STATE] = SD_EXITED_OK; return 1 }
181 job[SD_J_STATE] = SD_CRASHED
182 return 2
183 }
184 // killed by a signal (e.g. our SIGKILL) -> crashed
185 job[SD_J_EXIT] = 0 - (lo & 0x7F)
186 job[SD_J_STATE] = SD_CRASHED
187 return 2
188}
189
190// THE M5 BIND: dispatch a READY rung (M4) -> spawn it as a supervised job (M5). Maps the
191// leased business-key id to a numeric ws lane via id_to_ws (the heartbeat channel is keyed
192// by integer ws-id; the gate supplies the mapping for its hermetic fixture). Returns the
193// child pid (>0) on success, or -1 if nothing was dispatchable.
194func sd_dispatch_and_supervise(cx: *i64, dir: *u8, hbpath: *u8, job: *i64,
195 ws_id: i64, payload: i64, beat_epoch: i64,
196 id_out: *u8, fd_out: *i64) -> i64 {
197 let fd: i64 = dl_dispatch(cx, dir, id_out, fd_out)
198 if fd < 0 { return 0 - 1 } // nothing ready / all leased
199 job[SD_J_WS] = ws_id
200 let pid: i64 = sd_spawn_supervised(hbpath, job, payload, beat_epoch)
201 return pid
202}
203
204// THE RESTART DECISION (inlined supervisor policy + M1 evidence). Reads hbm_scan over the
205// single ws-id at the injected `now`/`thresh`:
206// - STALLED (missing heartbeat past threshold) AND restarts < budget -> RESPAWN (a fresh
207// supervised run with a fresh beat), restarts++, return ACTION_RESTART.
208// - STALLED AND budget exhausted -> mark ESCALATED, return ACTION_ESCALATE (no respawn).
209// - NOT stalled (alive, or it exited cleanly = EXITED_OK so it stopped beating on PURPOSE)
210// -> return ACTION_NONE. This is the no-thrash neg-control: a normally-completed job is
211// NEVER restarted. A naive M5 that respawns on "pid not alive" would WRONGLY restart the
212// clean job here (and FAIL the gate's T6).
213// `respawn_payload` selects the payload for the restarted run (the gate restarts with a
214// clean payload to prove the SECOND supervised run comes up ALIVE).
215func sd_supervise_tick(hbpath: *u8, now: i64, thresh: i64, job: *i64,
216 respawn_payload: i64, respawn_epoch: i64) -> i64 {
217 // If the job exited cleanly on its own, it is DONE -- never restart (the no-thrash law).
218 if job[SD_J_STATE] == SD_EXITED_OK { return SD_ACTION_NONE }
219
220 let ids: *i64 = sys_mmap(8) as *i64
221 ids[0] = job[SD_J_WS]
222 let vd: *i64 = sys_mmap(8) as *i64
223 let outs: *i64 = sys_mmap(32) as *i64
224 hbm_scan(hbpath, now, thresh, ids, 1, vd, outs)
225 let verdict: i64 = vd[0]
226
227 if verdict == HB_STALLED {
228 if job[SD_J_RESTARTS] < SD_MAXRESTART {
229 // RESTART / reassign: a fresh supervised run + a fresh beat clears the stall.
230 job[SD_J_RESTARTS] = job[SD_J_RESTARTS] + 1
231 sd_spawn_supervised(hbpath, job, respawn_payload, respawn_epoch)
232 return SD_ACTION_RESTART
233 }
234 // budget exhausted -> ESCALATE (bounded: no infinite restart loop / car-alarm storm)
235 job[SD_J_STATE] = SD_ESCALATED
236 return SD_ACTION_ESCALATE
237 }
238 // ALIVE (or unknown) -> nothing to do.
239 return SD_ACTION_NONE
240}
241
242// status emit: ONE buffer -> ONE locked fa_appendz (the cn_emit_* torn-line discipline).
243// "SUPDISP verb=<v> ws=<n> pid=<n> restarts=<n> action=<n> verdict=GREEN|RED"
244func sd_emit(path: *u8, verb: *u8, ws: i64, pid: i64, restarts: i64, action: i64,
245 ok: i64) -> i64 {
246 let buf: *u8 = sys_mmap(SD_REC_CAP + 16)
247 var o: i64 = 0
248 o = fa_cat(buf, o, "SUPDISP verb=\x00" as *u8)
249 o = fa_cat(buf, o, verb)
250 o = fa_cat(buf, o, " ws=\x00" as *u8)
251 o = fa_catn(buf, o, ws)
252 o = fa_cat(buf, o, " pid=\x00" as *u8)
253 o = fa_catn(buf, o, pid)
254 o = fa_cat(buf, o, " restarts=\x00" as *u8)
255 o = fa_catn(buf, o, restarts)
256 o = fa_cat(buf, o, " action=\x00" as *u8)
257 o = fa_catn(buf, o, action)
258 o = fa_cat(buf, o, " verdict=\x00" as *u8)
259 if ok == 1 { o = fa_cat(buf, o, "GREEN\x00" as *u8) } else { o = fa_cat(buf, o, "RED\x00" as *u8) }
260 buf[o] = 0 as u8
261 return fa_appendz(path, buf, SD_REC_CAP)
262}
263
264func sd_p(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
265func sd_n(v: i64) -> i64 { let bb: *u8=sys_mmap(28); var m: i64=v; if m<0{m=0-m; sys_write(1,"-\x00" as *u8,1)}; let t: *u8=sys_mmap(28); var k: i64=0; if m==0{t[0]=48 as u8;k=1}; while m>0{t[k]=(48+(m%10)) as u8;m=m/10;k=k+1}; var i: i64=0; while i<k{bb[i]=t[k-1-i];i=i+1}; sys_write(1,bb,k); return 0 }
266
267// CLI probe (NOT a daemon): self-author one supervised CLEAN job over a hermetic scratch
268// queue+heartbeat channel, poll it to completion, emit a status row. Proves the M5 organ
269// is live end-to-end on its own (the gate is the real referee). Time is real wall-clock
270// here (a smoke), but the GATE injects time for determinism.
271func main() -> i64 {
272 sd_p("=== nx_supervised_dispatch (M5) self-probe: spawn a supervised CLEAN job, poll, emit ===\n\x00" as *u8)
273 let ms: i64 = sys_now_realtime_ms()
274 let hb: *u8 = sys_mmap(96)
275 var o: i64 = 0
276 o = fa_cat(hb, o, "/tmp/sd_selfprobe_\x00" as *u8)
277 o = fa_catn(hb, o, ms)
278 o = fa_cat(hb, o, ".log\x00" as *u8)
279 hb[o] = 0 as u8
280
281 let job: *i64 = sd_job_new()
282 job[SD_J_WS] = 1
283 let epoch: i64 = sys_now_realtime_sec()
284 let pid: i64 = sd_spawn_supervised(hb, job, SD_PAY_CLEAN, epoch)
285 sd_p(" spawned supervised pid=\x00" as *u8); sd_n(pid)
286 sd_p(" limit_as=\x00" as *u8); sd_n(job[SD_J_LIMIT]); sd_p("\n\x00" as *u8)
287
288 // poll to completion (bounded loop; the clean payload exits promptly)
289 var guard: i64 = 0
290 var done: i64 = 0
291 while done == 0 {
292 let r: i64 = sd_poll(job)
293 if r != 0 { done = 1 }
294 guard = guard + 1
295 if guard > SD_MAGIC_100000 { done = 1 }
296 if done == 0 { sys_sleep_ms(2) }
297 }
298 sd_p(" job state=\x00" as *u8); sd_n(job[SD_J_STATE])
299 sd_p(" exit=\x00" as *u8); sd_n(job[SD_J_EXIT]); sd_p("\n\x00" as *u8)
300 sd_emit("knowledge/status/supervised_dispatch.log\x00" as *u8, "selfprobe\x00" as *u8,
301 job[SD_J_WS], pid, job[SD_J_RESTARTS], SD_ACTION_NONE, 1)
302 sd_p(" SUPERVISED-DISPATCH self-probe: filed (knowledge/status/supervised_dispatch.log)\n\x00" as *u8)
303 sys_exit(0)
304 return 0
305}