code wiki / (root) / nx_jobclaim_reap.nx

nx_jobclaim_reap.nx source

↩ module page · 185 lines · 8419 B

1// nx_jobclaim_reap.nx -- THE SANCTIONED WAY TO RETIRE A DEAD _jobs CLAIM. 2// 3// WHY THIS EXISTS (debt 1787259805, measured 2026-08-20). nx_joblost_gate correctly reports LOST=4 4// against a ratchet floor of 1 and is therefore RED. The four claims are real: ids 1786132743, 5// 1786239344, 1786732031 and 1786927039, aged 3.8 to 13.0 days, reservations that were made and 6// never came back. THE GATE IS CORRECT AND THE FLOOR MUST NOT BE RAISED -- loosening a ratchet to 7// flatter a number is editing the detector instead of fixing what it found. 8// The blocker was that the estate had NO VERB for removing a dead claim: nx_retire_path refuses 9// `_jobs/x` as too shallow, and nx_capsearch over 1,187 tools finds no incumbent (nx_orphan_reap 10// reaps DAEMONS by cmdline, nx_tmpstore_reap reaps seg-store FAMILIES, nx_claims reap works the 11// session-lease JOURNAL, nx_toolreg_reap works allowlist ROWS). Four things were blocked by one 12// missing verb: the gate cannot be bite-proven while its baseline is RED, so nx_gate_bite reports 13// UNCONTROLLED, so nx_gate_roster_run correctly refuses to admit it to the 630-row roster. 14// 15// THIS ORGAN IS THE ACTUATOR, NOT THE CENSUS. It takes ids and acts on exactly those. 16// The WORKLIST comes from nx_joblost_gate, which already scans the plane and prints 17// `LOST job_<id> age=<n>s`. Building a second directory scanner here would be a duplicate ruler, 18// and a sweep verb would make every reap an unnamed bulk act instead of an auditable one. 19// 20// IT DOES NOT DELETE. It APPENDS a tombstone (rule 13): the claim keeps its original marker and 21// gains `state=REAPED ts=<now> age=<n>s by=nx_jobclaim_reap`. The reader that must understand that 22// tombstone -- nx_joblost_gate -- imports the SAME classifier from nx_jobclaim_lib, because a 23// reaper shipped alone is a no-op that looks like a fix. 24// 25// EVERY REFUSAL NAMES ITS RULE. ABSENT, DONE, HAS-OUT, TOO-YOUNG and UNPARSED are five different 26// answers with five different remedies; a single "refused" would send the reader to re-derive by 27// hand the examination this organ just performed. 28// 29// license_tier: ORIGINAL expect_exit: 0 30import "nx_syscalls.nx" 31import "nx_jobclaim_lib.nx" 32 33const JRC_EXIT_OK: i64 = 0 34const JRC_EXIT_REFUSED: i64 = 1 35const JRC_EXIT_USAGE: i64 = 2 36const JRC_EXIT_BLOCKED: i64 = 3 37const JRC_ARGV_VERB: i64 = 1 38const JRC_ARGV_FIRST: i64 = 2 39const JRC_MIN_ARGC: i64 = 3 40 41func jrc_w(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 42 43func jrc_n(v: i64) -> i64 { 44 let b: *u8 = sys_mmap(JR_TOMB_MAX) 45 let p: i64 = jr_catnum(b, 0, v) 46 sys_write(1, b, p) 47 return 0 48} 49 50func jrc_usage() -> i64 { 51 jrc_w("usage: nx_jobclaim_reap check <job-id> [<job-id>...] [maxage=<sec>] [dir=<path>]\n" as *u8) 52 jrc_w(" nx_jobclaim_reap reap <job-id> [<job-id>...] [maxage=<sec>] [dir=<path>]\n" as *u8) 53 jrc_w(" check DECIDES and writes nothing; reap APPENDS a tombstone to every eligible claim.\n" as *u8) 54 jrc_w(" The WORKLIST is nx_joblost_gate's LOST rows -- this organ actuates them, it is not a census.\n" as *u8) 55 jrc_w(" dir defaults to _jobs and is refused outside _jobs and /tmp/ (blast radius).\n" as *u8) 56 jrc_w(" exit 0 every named id reaped or already reaped | 1 at least one refusal | 2 usage | 3 dir refused\n" as *u8) 57 return 0 58} 59 60// "<key>=<value>" -> pointer to <value>, or 0 when this argument is not that key. 61func jrc_opt(arg: *u8, key: *u8) -> *u8 { 62 let k: i64 = jr_slen(key) 63 let n: i64 = jr_slen(arg) 64 if n <= k { return 0 as *u8 } 65 var i: i64 = 0 66 while i < k { if arg[i] != key[i] { return 0 as *u8 } i = i + 1 } 67 return ((arg as i64) + k) as *u8 68} 69 70func main(argc: i64, argv: *i64) -> i64 { 71 if argc < JRC_MIN_ARGC { jrc_usage(); sys_exit(JRC_EXIT_USAGE) } 72 73 let verb: *u8 = argv[JRC_ARGV_VERB] as *u8 74 var dry: i64 = 0 - 1 75 if jr_streq(verb, "check" as *u8) == 1 { dry = 1 } 76 if jr_streq(verb, "reap" as *u8) == 1 { dry = 0 } 77 if dry < 0 { 78 jrc_w("REFUSED-BAD-VERB: \"" as *u8); jrc_w(verb) 79 jrc_w("\" -- the verbs are check and reap\n" as *u8) 80 jrc_usage() 81 sys_exit(JRC_EXIT_USAGE) 82 } 83 84 // ---- options first, so the threshold and the directory are settled before any id is examined. 85 let dflt: *i64 = sys_mmap(JR_SCRATCH) as *i64 86 let sc: *i64 = sys_mmap(JR_SCRATCH) as *i64 87 var maxage: i64 = jr_maxage(dflt, sc) 88 var maxage_src: *u8 = "conf" as *u8 89 if dflt[0] == 1 { maxage_src = "code-default (conf absent or unparseable, stated not hidden)" as *u8 } 90 var dir: *u8 = JR_JOBS 91 92 var i: i64 = JRC_ARGV_FIRST 93 while i < argc { 94 let a: *u8 = argv[i] as *u8 95 let mv: *u8 = jrc_opt(a, "maxage=" as *u8) 96 if (mv as i64) != 0 { 97 let v: i64 = jr_atoi_strict(mv) 98 if v < 0 { 99 jrc_w("REFUSED-BAD-MAXAGE: \"" as *u8); jrc_w(mv) 100 jrc_w("\" is not a positive integer -- a zero or negative threshold would reap everything\n" as *u8) 101 sys_exit(JRC_EXIT_USAGE) 102 } 103 maxage = v 104 maxage_src = "argv (highest priority in the configuration hierarchy)" as *u8 105 } 106 let dv: *u8 = jrc_opt(a, "dir=" as *u8) 107 if (dv as i64) != 0 { dir = dv } 108 i = i + 1 109 } 110 111 if jr_dir_allowed(dir) == 0 { 112 jrc_w("REFUSED-DIR: \"" as *u8); jrc_w(dir) 113 jrc_w("\" -- this organ writes only inside the _jobs plane or under /tmp/ , and never through \"..\".\n" as *u8) 114 jrc_w(" A reaper that will append anywhere is a different and much larger tool than this one.\n" as *u8) 115 sys_exit(JRC_EXIT_BLOCKED) 116 } 117 118 let now: i64 = sys_now_realtime_sec() 119 jrc_w("=== NX-JOBCLAIM-REAP verb=" as *u8); jrc_w(verb) 120 jrc_w(" dir=" as *u8); jrc_w(dir) 121 jrc_w(" now=" as *u8); jrc_n(now) 122 jrc_w(" maxage=" as *u8); jrc_n(maxage) 123 jrc_w("s from " as *u8); jrc_w(maxage_src) 124 jrc_w(" ===\n" as *u8) 125 if dry == 1 { jrc_w(" DRY RUN -- deciding only, nothing is written.\n" as *u8) } 126 127 let info: *i64 = sys_mmap(JR_I_SLOTS * JR_I64_BYTES) as *i64 128 let pbuf: *u8 = sys_mmap(JR_PATH) 129 let line: *u8 = sys_mmap(JR_LINE) 130 131 var examined: i64 = 0 132 var reaped: i64 = 0 133 var already: i64 = 0 134 var refused: i64 = 0 135 136 i = JRC_ARGV_FIRST 137 while i < argc { 138 let a: *u8 = argv[i] as *u8 139 var is_opt: i64 = 0 140 if (jrc_opt(a, "maxage=" as *u8) as i64) != 0 { is_opt = 1 } 141 if (jrc_opt(a, "dir=" as *u8) as i64) != 0 { is_opt = 1 } 142 if is_opt == 0 { 143 examined = examined + 1 144 let id: i64 = jr_atoi_strict(a) 145 var d: i64 = JR_D_BADID 146 if id > 0 { d = jr_reap_one(dir, id, now, maxage, dry, info, pbuf, JR_PATH, sc, line, JR_LINE) } 147 148 jrc_w(" " as *u8); jrc_w(jr_decision_name(d)) 149 jrc_w(" job_" as *u8); jrc_w(a) 150 if id > 0 { 151 jrc_w(" state=" as *u8); jrc_w(jr_state_name(info[JR_I_STATE])) 152 jrc_w(" bytes=" as *u8); jrc_n(info[JR_I_BYTES]) 153 jrc_w(" age=" as *u8); jrc_n(info[JR_I_AGE]) 154 jrc_w("s has_out=" as *u8); jrc_n(info[JR_I_HASOUT]) 155 } 156 jrc_w("\n " as *u8); jrc_w(jr_decision_why(d)); jrc_w("\n" as *u8) 157 158 if d == JR_D_REAP { reaped = reaped + 1 } 159 if d == JR_D_ALREADY { already = already + 1 } 160 if d > JR_D_ALREADY { refused = refused + 1 } 161 } 162 i = i + 1 163 } 164 165 if examined == 0 { 166 jrc_w("REFUSED-NO-IDS: every argument was an option -- name at least one job id\n" as *u8) 167 jrc_usage() 168 sys_exit(JRC_EXIT_USAGE) 169 } 170 171 // A PARTITION IS A CLAIM: CHECK THE PARTS SUM, and print the sum so a reader can check it too. 172 let sum: i64 = reaped + already + refused 173 jrc_w(" examined=" as *u8); jrc_n(examined) 174 if dry == 1 { jrc_w(" would_reap=" as *u8) } else { jrc_w(" reaped=" as *u8) } 175 jrc_n(reaped) 176 jrc_w(" already=" as *u8); jrc_n(already) 177 jrc_w(" refused=" as *u8); jrc_n(refused) 178 jrc_w(" sum=" as *u8); jrc_n(sum) 179 if sum == examined { jrc_w(" partition=RECONCILES\n" as *u8) } else { jrc_w(" partition=LEAKS\n" as *u8) } 180 if sum != examined { sys_exit(JRC_EXIT_BLOCKED) } 181 182 if refused > 0 { sys_exit(JRC_EXIT_REFUSED) } 183 sys_exit(JRC_EXIT_OK) 184 return 0 185}