code wiki / (root) / nx_jobcancel_lib.nx

nx_jobcancel_lib.nx source

↩ module page · 233 lines · 10634 B

1// nx_jobcancel_lib.nx -- THE ONE cancel path for the jobs nx_job_run launched (engineshift ES29, 2026-09-15). 2// 3// WHY. MEASURED 2026-09-15: two cross-encoder one-shots launched through nx_job_run held the estate host at load 27 4// on 8 cpus for over 30 hours with 0 bytes of output, and admission correctly refused every build -- including the 5// builds that would have replaced them. No sovereign surface could stop them: the launcher had no cancel verb, the 6// resource governor's law forbids recycling a one-shot (never terminate what nothing will restart), and the 7// /proc-walking diagnostics refuse to run above the very load the runaway causes. A RUNAWAY JOB THAT CANNOT BE 8// CANCELLED IS A RESOURCE INCIDENT WITH NO REMEDY. This lib is the remedy, and it is BOUNDED BY CONSTRUCTION: 9// * it cancels only a pid the launcher's own journal recorded for that out-file (REFUSED-ABSENT otherwise); 10// * it signals only if /proc/<pid>/cmdline argv[0] is the elf the journal recorded (REFUSED-MISMATCH otherwise -- 11// a recycled pid, a stranger, a daemon: never touched); 12// * TERM first, a named grace (knowledge/jobrun.conf cancel_grace_ms, bootstrap default below), then KILL, then 13// one more grace; every outcome is journaled beside the launch it cancels, so the record reads 14// launch -> cancel -> outcome in one file. 15// The journal row: <kind> TAB <epoch> TAB <pid> TAB <elf> TAB <out-file> TAB <name-or-outcome>, kind = launch | cancel. 16// license_tier: ORIGINAL. No hw writes (Rule 26). 17import "nx_syscalls.nx" 18import "nx_itoa_lib.nx" 19 20const JC_JRNL: *u8 = "knowledge/status/jobrun.jrnl" 21const JC_CONF: *u8 = "knowledge/jobrun.conf" 22const JC_CONF_KEY: *u8 = "cancel_grace_ms" 23const JC_GRACE_MS_DEFAULT: i64 = 3000 // bootstrap default (rule 17: env > svc-config > bootstrap > code); the conf row overrides 24const JC_POLL_MS: i64 = 100 25const JC_SIGTERM: i64 = 15 26const JC_SIGKILL: i64 = 9 27const JC_CMDLINE_CAP: i64 = 4096 // /proc/<pid>/cmdline has no size (lseek END reads 0), so ONE bounded read; only argv[0] is compared, a longer cmdline is compared on its prefix and that is announced by JC_CMDLINE_CAP in the receipt 28const JC_PATH_CAP: i64 = 128 29const JC_ROW_CAP: i64 = 2048 30const JC_MODE_644: i64 = 420 31const JC_TAB: i64 = 9 32const JC_LF: i64 = 10 33const JC_SPACE: i64 = 32 34const JC_CH_ZERO: i64 = 48 35const JC_CH_NINE: i64 = 57 36const JC_OK_TERM: i64 = 0 37const JC_OK_KILL: i64 = 1 38const JC_REFUSED_ABSENT: i64 = 2 39const JC_REFUSED_MISMATCH: i64 = 3 40const JC_ALREADY_GONE: i64 = 4 41const JC_STILL_ALIVE: i64 = 5 42const JC_F_KIND: i64 = 0 43const JC_F_EPOCH: i64 = 1 44const JC_F_PID: i64 = 2 45const JC_F_ELF: i64 = 3 46const JC_F_OUT: i64 = 4 47const JC_F_NAME: i64 = 5 48const JC_KIND_LAUNCH: *u8 = "launch" 49const JC_KIND_CANCEL: *u8 = "cancel" 50 51func jc_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 52func jc_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { d[o + i] = s[i]; i = i + 1 } return o + i } 53// a NUL-terminated string equals the byte run s[0..n) 54func jc_eq_n(a: *u8, s: *u8, n: i64) -> i64 { 55 if jc_slen(a) != n { return 0 } 56 var i: i64 = 0 57 while i < n { if a[i] != s[i] { return 0 } i = i + 1 } 58 return 1 59} 60func jc_streq(a: *u8, b: *u8) -> i64 { return jc_eq_n(a, b, jc_slen(b)) } 61func jc_atoi_n(s: *u8, n: i64) -> i64 { 62 var v: i64 = 0 63 var i: i64 = 0 64 while i < n { 65 let c: i64 = s[i] as i64 66 if c < JC_CH_ZERO { return v } 67 if c > JC_CH_NINE { return v } 68 v = v * 10 + (c - JC_CH_ZERO) 69 i = i + 1 70 } 71 return v 72} 73// field k of a TAB row [line, line+n): offset and length; 1 when present 74func jc_field(line: *u8, n: i64, k: i64, off: *i64, len: *i64) -> i64 { 75 var f: i64 = 0 76 var s: i64 = 0 77 var i: i64 = 0 78 while i <= n { 79 var hit: i64 = 0 80 if i == n { hit = 1 } else { if line[i] == (JC_TAB as u8) { hit = 1 } } 81 if hit == 1 { 82 if f == k { off[0] = s; len[0] = i - s; return 1 } 83 f = f + 1 84 s = i + 1 85 } 86 i = i + 1 87 } 88 return 0 89} 90func jc_procpath(out: *u8, pid: i64, suffix: *u8) -> i64 { 91 var o: i64 = jc_cat(out, 0, "/proc/" as *u8) 92 o = ccz_cat_num(out, o, pid) 93 out[o] = 47 as u8; o = o + 1 94 o = jc_cat(out, o, suffix) 95 out[o] = 0 as u8 96 return o 97} 98// is there a process at pid at all (a zombie still answers yes here; jc_verify tells it apart) 99func jc_alive(pid: i64) -> i64 { 100 let p: *u8 = sys_mmap(JC_PATH_CAP) 101 jc_procpath(p, pid, "stat" as *u8) 102 let fd: i64 = sys_openat_rd(p) 103 if fd < 0 { return 0 } 104 sys_close(fd) 105 return 1 106} 107// ONE bounded read of /proc/<pid>/cmdline into buf; bytes read, or -1 when the process is gone 108func jc_cmdline(pid: i64, buf: *u8, cap: i64) -> i64 { 109 let p: *u8 = sys_mmap(JC_PATH_CAP) 110 jc_procpath(p, pid, "cmdline" as *u8) 111 let fd: i64 = sys_openat_rd(p) 112 if fd < 0 { return 0 - 1 } 113 var n: i64 = 0 114 var r: i64 = 1 115 while r > 0 { if n < cap { r = sys_read(fd, (buf as i64 + n) as *u8, cap - n); if r > 0 { n = n + r } } else { r = 0 } } 116 sys_close(fd) 117 return n 118} 119// 1 = argv[0] of the live process IS the elf the journal recorded; 0 = a different process (or a zombie, whose 120// cmdline is empty) sits at that pid; -1 = no process at that pid 121func jc_verify(pid: i64, elf: *u8) -> i64 { 122 let buf: *u8 = sys_mmap(JC_CMDLINE_CAP) 123 let n: i64 = jc_cmdline(pid, buf, JC_CMDLINE_CAP) 124 if n < 0 { return 0 - 1 } 125 if n == 0 { return 0 } 126 var a0: i64 = 0 127 while a0 < n { if buf[a0] == (0 as u8) { break } a0 = a0 + 1 } 128 if jc_eq_n(elf, buf, a0) == 1 { return 1 } 129 return 0 130} 131// the LAST launch row whose out-file is outfile: pid into pidp, elf copied into elfbuf; 1 found / 0 absent 132func jc_find(jrnl: *u8, outfile: *u8, pidp: *i64, elfbuf: *u8, elfcap: i64) -> i64 { 133 let nb: *i64 = sys_mmap(16) as *i64 134 nb[0] = 0 135 let buf: *u8 = sys_read_file(jrnl, nb) 136 if (buf as i64) == 0 { return 0 } 137 let n: i64 = nb[0] 138 let off: *i64 = sys_mmap(16) as *i64 139 let len: *i64 = sys_mmap(16) as *i64 140 var found: i64 = 0 141 var p: i64 = 0 142 while p < n { 143 var e: i64 = p 144 while e < n { if buf[e] == (JC_LF as u8) { break } e = e + 1 } 145 let line: *u8 = (buf as i64 + p) as *u8 146 let ln: i64 = e - p 147 p = e + 1 148 if jc_field(line, ln, JC_F_KIND, off, len) == 1 { if jc_eq_n(JC_KIND_LAUNCH, (line as i64 + off[0]) as *u8, len[0]) == 1 { 149 if jc_field(line, ln, JC_F_OUT, off, len) == 1 { if jc_eq_n(outfile, (line as i64 + off[0]) as *u8, len[0]) == 1 { 150 if jc_field(line, ln, JC_F_PID, off, len) == 1 { pidp[0] = jc_atoi_n((line as i64 + off[0]) as *u8, len[0]) } 151 if jc_field(line, ln, JC_F_ELF, off, len) == 1 { 152 var l: i64 = len[0] 153 if l >= elfcap { l = elfcap - 1 } 154 var i: i64 = 0 155 while i < l { elfbuf[i] = line[off[0] + i]; i = i + 1 } 156 elfbuf[l] = 0 as u8 157 } 158 found = 1 159 } } 160 } } 161 } 162 return found 163} 164// append one row; fail-soft (bookkeeping must never break the work): 1 written, 0 not 165func jc_journal(jrnl: *u8, kind: *u8, epoch: i64, pid: i64, elf: *u8, outfile: *u8, tail: *u8) -> i64 { 166 let fd: i64 = sys_openat_append(jrnl, JC_MODE_644) 167 if fd < 0 { return 0 } 168 let row: *u8 = sys_mmap(JC_ROW_CAP) 169 var o: i64 = jc_cat(row, 0, kind); row[o] = JC_TAB as u8; o = o + 1 170 o = ccz_cat_num(row, o, epoch); row[o] = JC_TAB as u8; o = o + 1 171 o = ccz_cat_num(row, o, pid); row[o] = JC_TAB as u8; o = o + 1 172 o = jc_cat(row, o, elf); row[o] = JC_TAB as u8; o = o + 1 173 o = jc_cat(row, o, outfile); row[o] = JC_TAB as u8; o = o + 1 174 o = jc_cat(row, o, tail); row[o] = JC_LF as u8; o = o + 1 175 sys_write(fd, row, o) 176 sys_close(fd) 177 return 1 178} 179// `cancel_grace_ms <n>` from the conf; the default when the conf or the row is absent 180func jc_conf_grace(conf: *u8, dflt: i64) -> i64 { 181 let nb: *i64 = sys_mmap(16) as *i64 182 nb[0] = 0 183 let buf: *u8 = sys_read_file(conf, nb) 184 if (buf as i64) == 0 { return dflt } 185 let n: i64 = nb[0] 186 let kl: i64 = jc_slen(JC_CONF_KEY) 187 var p: i64 = 0 188 while p < n { 189 var e: i64 = p 190 while e < n { if buf[e] == (JC_LF as u8) { break } e = e + 1 } 191 if e - p > kl { if jc_eq_n(JC_CONF_KEY, (buf as i64 + p) as *u8, kl) == 1 { if buf[p + kl] == (JC_SPACE as u8) { 192 let v: i64 = jc_atoi_n((buf as i64 + p + kl + 1) as *u8, e - p - kl - 1) 193 if v > 0 { return v } 194 } } } 195 p = e + 1 196 } 197 return dflt 198} 199func jc_outcome_name(code: i64) -> *u8 { 200 if code == JC_OK_TERM { return "CANCELLED" as *u8 } 201 if code == JC_OK_KILL { return "CANCELLED-KILLED" as *u8 } 202 if code == JC_REFUSED_ABSENT { return "REFUSED-ABSENT" as *u8 } 203 if code == JC_REFUSED_MISMATCH { return "REFUSED-MISMATCH" as *u8 } 204 if code == JC_ALREADY_GONE { return "ALREADY-GONE" as *u8 } 205 return "STILL-ALIVE" as *u8 206} 207// wait up to grace_ms for the journaled elf to leave the pid; 1 when it left 208func jc_wait_gone(pid: i64, elf: *u8, grace_ms: i64, poll_ms: i64) -> i64 { 209 var waited: i64 = 0 210 while waited < grace_ms { 211 sys_sleep_ms(poll_ms) 212 waited = waited + poll_ms 213 if jc_verify(pid, elf) != 1 { return 1 } 214 } 215 return 0 216} 217// THE VERB. Returns a JC_* code; pidp carries the pid acted on (0 when none). Every outcome is journaled. 218func jc_cancel(jrnl: *u8, outfile: *u8, grace_ms: i64, poll_ms: i64, pidp: *i64) -> i64 { 219 pidp[0] = 0 220 let elf: *u8 = sys_mmap(JC_ROW_CAP) 221 if jc_find(jrnl, outfile, pidp, elf, JC_ROW_CAP) == 0 { return JC_REFUSED_ABSENT } 222 let pid: i64 = pidp[0] 223 let now: i64 = sys_now_realtime_sec() 224 let v: i64 = jc_verify(pid, elf) 225 if v < 0 { jc_journal(jrnl, JC_KIND_CANCEL, now, pid, elf, outfile, jc_outcome_name(JC_ALREADY_GONE)); return JC_ALREADY_GONE } 226 if v == 0 { jc_journal(jrnl, JC_KIND_CANCEL, now, pid, elf, outfile, jc_outcome_name(JC_REFUSED_MISMATCH)); return JC_REFUSED_MISMATCH } 227 nx_kill(pid, JC_SIGTERM) 228 if jc_wait_gone(pid, elf, grace_ms, poll_ms) == 1 { jc_journal(jrnl, JC_KIND_CANCEL, now, pid, elf, outfile, jc_outcome_name(JC_OK_TERM)); return JC_OK_TERM } 229 nx_kill(pid, JC_SIGKILL) 230 if jc_wait_gone(pid, elf, grace_ms, poll_ms) == 1 { jc_journal(jrnl, JC_KIND_CANCEL, now, pid, elf, outfile, jc_outcome_name(JC_OK_KILL)); return JC_OK_KILL } 231 jc_journal(jrnl, JC_KIND_CANCEL, now, pid, elf, outfile, jc_outcome_name(JC_STILL_ALIVE)) 232 return JC_STILL_ALIVE 233}