code wiki / (root) / nx_jobclaim_reap_gate.nx

nx_jobclaim_reap_gate.nx source

↩ module page · 285 lines · 16261 B

1// nx_jobclaim_reap_gate.nx -- BITE-PROOF for the dead-claim reaper, both directions. 2// 3// IT DRIVES THE SHIPPING CODE. Every tooth calls nx_jobclaim_lib directly -- the same jr_state_of, 4// jr_decide, jr_examine and jr_tombstone that nx_jobclaim_reap and nx_joblost_gate call. A tooth 5// that tests a re-implementation proves nothing about what ships. 6// 7// THE ANTI-VACUITY TOOTH IS FIRST AND IT IS THE AGE AXIS. Two claims with byte-identical contents, 8// differing ONLY in age, must decide differently. A reaper that ignored age would fire on both; a 9// reaper that refused everything would fire on neither -- and a guard that refuses everything passes 10// every negative test, which is why T10 carries a POSITIVE control beside its refusals. 11// 12// T3 IS THE TOOTH THIS WHOLE CHANGE EXISTS FOR. The tombstone is APPENDED, so a reaped claim still 13// contains `state=CLAIMED`. T3 asserts BOTH markers are present in the same bytes AND that the 14// shared classifier still answers REAPED. If the classifier ever tests CLAIMED before REAPED, T3 is 15// the tooth that goes red -- and without it the reaper is a no-op that looks like a fix. 16// 17// FIXTURES ARE BUILT AT RUNTIME UNDER /tmp/<gate>/ and their ids are DERIVED FROM `now`, so the age 18// axis under test is real rather than frozen. Setup unlinks every fixture path before creating it, 19// so the gate is idempotent: a gate that is not idempotent reports on its first run and lies about 20// every run after. 21// 22// license_tier: ORIGINAL expect_exit: 0 23import "nx_syscalls.nx" 24import "nx_gate_verdict.nx" 25import "nx_jobclaim_lib.nx" 26 27const JG_DIR: *u8 = "/tmp/nx_jobclaim_reap_gate" as *u8 28const JG_FIX: *u8 = "/tmp/nx_jobclaim_reap_gate/f" as *u8 29const JG_CLAIMED: *u8 = "state=CLAIMED ts=fixture\n" as *u8 30const JG_DONE: *u8 = "state=DONE rc=0 exit=0 bytes=12\n" as *u8 31const JG_GARBAGE: *u8 = "this claim matches no marker the classifier knows\n" as *u8 32const JG_OUTBODY: *u8 = "the worker produced this\n" as *u8 33const JG_EMPTY: *u8 = "" as *u8 34 35// Fixture id offsets BELOW old_id. Distinct so every scenario is a separate file in one directory; 36// derived from old_id rather than typed as absolute ids, which would drift out of the OLD band the 37// moment the shared threshold changed. 38const JG_OFF_CLAIMED: i64 = 0 39const JG_OFF_EMPTYCLM: i64 = 1 40const JG_OFF_DONE: i64 = 2 41const JG_OFF_HASOUT: i64 = 3 42const JG_OFF_GARBAGE: i64 = 4 43const JG_OFF_ABSENT: i64 = 5 44const JG_AGE_MULT: i64 = 2 // old_id sits maxage*JG_AGE_MULT in the past: unambiguously OLD 45const JG_YOUNG_DIV: i64 = 2 // young_id sits maxage/JG_YOUNG_DIV in the past: unambiguously YOUNG 46 47func jg_write(path: *u8, body: *u8) -> i64 { 48 let fd: i64 = sys_openat_wr(path, MODE_0644) 49 if fd < 0 { return 0 - 1 } 50 let n: i64 = jr_slen(body) 51 if n > 0 { sys_write(fd, body, n) } 52 sys_close(fd) 53 return n 54} 55 56// Byte length, or -1 when the file cannot be opened. ABSENT and EMPTY are different answers here too. 57func jg_size(path: *u8, sc: *i64) -> i64 { 58 let b: *u8 = sys_read_file(path, sc) 59 if (b as i64) == 0 { return 0 - 1 } 60 let n: i64 = sc[0] 61 sys_free_file(b, n) 62 return n 63} 64 65// Does the file contain this marker? -1 when absent, so "cannot look" never reads as "not present". 66func jg_holds(path: *u8, pat: *u8, sc: *i64) -> i64 { 67 let b: *u8 = sys_read_file(path, sc) 68 if (b as i64) == 0 { return 0 - 1 } 69 let n: i64 = sc[0] 70 let h: i64 = jr_has(b, n, pat) 71 sys_free_file(b, n) 72 return h 73} 74 75// Plant one fixture claim: unlink first (idempotence), then write. Returns the planted byte count. 76func jg_plant(pbuf: *u8, id: i64, body: *u8) -> i64 { 77 if jr_mkpath(pbuf, JR_PATH, JG_FIX, id, JR_EXT_CLAIM) < 0 { return 0 - 1 } 78 sys_unlinkat(pbuf) 79 return jg_write(pbuf, body) 80} 81 82func main(argc: i64, argv: *i64) -> i64 { 83 let ctr: *i64 = gv_ctr() 84 gv_head("=== NX-JOBCLAIM-REAP GATE -- the tombstone must land, and must refuse everything else ===" as *u8) 85 86 let sc: *i64 = sys_mmap(JR_SCRATCH) as *i64 87 let dflt: *i64 = sys_mmap(JR_SCRATCH) as *i64 88 let info: *i64 = sys_mmap(JR_I_SLOTS * JR_I64_BYTES) as *i64 89 let pbuf: *u8 = sys_mmap(JR_PATH) 90 let qbuf: *u8 = sys_mmap(JR_PATH) 91 let line: *u8 = sys_mmap(JR_LINE) 92 93 let maxage: i64 = jr_maxage(dflt, sc) 94 let now: i64 = sys_now_realtime_sec() 95 let old_id: i64 = now - (maxage * JG_AGE_MULT) 96 let young_id: i64 = now - (maxage / JG_YOUNG_DIV) 97 98 gv_puts(" shared threshold maxage=" as *u8); gv_num(maxage) 99 if dflt[0] == 1 { gv_puts("s from the CODE DEFAULT (conf absent or unparseable -- stated, not hidden)" as *u8) } 100 else { gv_puts("s from " as *u8); gv_puts(JR_MAXAGE_CONF) } 101 gv_puts("\n now=" as *u8); gv_num(now) 102 gv_puts(" old_id=" as *u8); gv_num(old_id) 103 gv_puts(" young_id=" as *u8); gv_num(young_id); gv_puts("\n" as *u8) 104 105 sys_mkdir(JG_DIR, MODE_0755) 106 sys_mkdir(JG_FIX, MODE_0755) 107 108 // ---- SETUP. Every path is unlinked before it is written, so a crashed previous run cannot 109 // become this run's evidence. CLEAR LEAKABLE STATE AT SETUP OR THE GATE MEASURES ITS PREDECESSOR. 110 let n_clm: i64 = jg_plant(pbuf, old_id - JG_OFF_CLAIMED, JG_CLAIMED) 111 let n_yng: i64 = jg_plant(pbuf, young_id, JG_CLAIMED) 112 let n_emp: i64 = jg_plant(pbuf, old_id - JG_OFF_EMPTYCLM, JG_EMPTY) 113 let n_dne: i64 = jg_plant(pbuf, old_id - JG_OFF_DONE, JG_DONE) 114 let n_out: i64 = jg_plant(pbuf, old_id - JG_OFF_HASOUT, JG_CLAIMED) 115 let n_gbg: i64 = jg_plant(pbuf, old_id - JG_OFF_GARBAGE, JG_GARBAGE) 116 // the ORPHAN case needs its sibling .out to exist 117 jr_mkpath(pbuf, JR_PATH, JG_FIX, old_id - JG_OFF_HASOUT, JR_EXT_OUT) 118 sys_unlinkat(pbuf) 119 let n_sib: i64 = jg_write(pbuf, JG_OUTBODY) 120 // the ABSENT case must NOT exist: unlink it and prove it is gone 121 jr_mkpath(pbuf, JR_PATH, JG_FIX, old_id - JG_OFF_ABSENT, JR_EXT_CLAIM) 122 sys_unlinkat(pbuf) 123 let absent_sz: i64 = jg_size(pbuf, sc) 124 125 let want_clm: i64 = jr_slen(JG_CLAIMED) 126 let want_dne: i64 = jr_slen(JG_DONE) 127 let want_gbg: i64 = jr_slen(JG_GARBAGE) 128 129 gv_puts(" planted: claimed=" as *u8); gv_num(n_clm) 130 gv_puts(" young=" as *u8); gv_num(n_yng) 131 gv_puts(" empty=" as *u8); gv_num(n_emp) 132 gv_puts(" done=" as *u8); gv_num(n_dne) 133 gv_puts(" hasout=" as *u8); gv_num(n_out) 134 gv_puts(" sibling_out=" as *u8); gv_num(n_sib) 135 gv_puts(" garbage=" as *u8); gv_num(n_gbg) 136 gv_puts(" absent_size=" as *u8); gv_num(absent_sz); gv_puts("\n\n" as *u8) 137 138 // ---- ASSERT THE FIXTURE REACHED ITS CONDITION BEFORE ASSERTING ANY OUTCOME. A fixture the 139 // defect cannot fail is not a test, and four vacuous fixtures were shipped in this estate in a 140 // single day by skipping exactly this step. 141 var built: i64 = 0 142 if n_clm == want_clm { if n_yng == want_clm { if n_emp == 0 { if n_dne == want_dne { 143 if n_out == want_clm { if n_gbg == want_gbg { if n_sib > 0 { if absent_sz < 0 { built = 1 } } } } } } } } 144 let looked: i64 = gv_need("every fixture reached its planted condition and the ABSENT id is genuinely absent" as *u8, built, ctr) 145 146 // ---- T1 THE BITE, AND IT IS THE AGE AXIS. Both claims carry byte-identical CLAIMED content and 147 // no sibling .out; ONLY the age differs. 148 let d_old: i64 = jr_examine(JG_FIX, old_id - JG_OFF_CLAIMED, now, maxage, info, pbuf, JR_PATH, sc) 149 let d_young: i64 = jr_examine(JG_FIX, young_id, now, maxage, info, pbuf, JR_PATH, sc) 150 var fired_old: i64 = 0 151 var fired_yng: i64 = 0 152 if d_old == JR_D_REAP { fired_old = 1 } 153 if d_young == JR_D_REAP { fired_yng = 1 } 154 gv_puts(" decisions: old=" as *u8); gv_puts(jr_decision_name(d_old)) 155 gv_puts(" young=" as *u8); gv_puts(jr_decision_name(d_young)); gv_puts("\n" as *u8) 156 gv_bite("T1 an OLD claim decides REAP while a YOUNG one with identical content does not -- only age differs" as *u8, 157 fired_old, fired_yng, ctr) 158 159 var t1b: i64 = 0 160 if d_young == JR_D_TOO_YOUNG { t1b = 1 } 161 gv_check("T1b the young claim is refused BY NAME (REFUSED-TOO-YOUNG), not by a generic no" as *u8, t1b, ctr) 162 163 // ---- T2 THE TOMBSTONE LANDS, and the file grew by exactly what the writer said it wrote. 164 jr_mkpath(qbuf, JR_PATH, JG_FIX, old_id - JG_OFF_CLAIMED, JR_EXT_CLAIM) 165 let before: i64 = jg_size(qbuf, sc) 166 let d_reap: i64 = jr_reap_one(JG_FIX, old_id - JG_OFF_CLAIMED, now, maxage, 0, info, pbuf, JR_PATH, sc, line, JR_LINE) 167 let after: i64 = jg_size(qbuf, sc) 168 let st_after: i64 = jr_state_of_path(qbuf, sc) 169 var t2: i64 = 0 170 if d_reap == JR_D_REAP { if after > before { if st_after == JR_ST_REAPED { t2 = 1 } } } 171 gv_puts(" tombstone: before=" as *u8); gv_num(before) 172 gv_puts(" after=" as *u8); gv_num(after) 173 gv_puts(" grew_by=" as *u8); gv_num(after - before) 174 gv_puts(" state=" as *u8); gv_puts(jr_state_name(st_after)); gv_puts("\n" as *u8) 175 gv_check("T2 reaping APPENDS a tombstone: the file grew and the shared classifier now reads REAPED" as *u8, t2, ctr) 176 177 // ---- T3 THE ORDER TOOTH. This is the tooth the whole two-organ change exists for. 178 let holds_reaped: i64 = jg_holds(qbuf, JR_MARK_REAPED, sc) 179 let holds_claimed: i64 = jg_holds(qbuf, JR_MARK_CLAIMED, sc) 180 var t3: i64 = 0 181 if holds_reaped == 1 { if holds_claimed == 1 { if st_after == JR_ST_REAPED { t3 = 1 } } } 182 gv_puts(" same bytes hold state=REAPED=" as *u8); gv_num(holds_reaped) 183 gv_puts(" and state=CLAIMED=" as *u8); gv_num(holds_claimed); gv_puts("\n" as *u8) 184 gv_check("T3 a reaped claim STILL contains state=CLAIMED and is classified REAPED anyway (order-first, the no-op trap)" as *u8, t3, ctr) 185 186 // ---- T4 IDEMPOTENCE. Rule 10: safe to run twice. A second reap must write NOTHING. 187 let d_again: i64 = jr_reap_one(JG_FIX, old_id - JG_OFF_CLAIMED, now, maxage, 0, info, pbuf, JR_PATH, sc, line, JR_LINE) 188 let after2: i64 = jg_size(qbuf, sc) 189 var t4: i64 = 0 190 if d_again == JR_D_ALREADY { if after2 == after { t4 = 1 } } 191 gv_puts(" second reap: " as *u8); gv_puts(jr_decision_name(d_again)) 192 gv_puts(" size " as *u8); gv_num(after); gv_puts(" -> " as *u8); gv_num(after2); gv_puts("\n" as *u8) 193 gv_check("T4 reaping twice is ALREADY-REAPED and appends nothing (idempotent, no duplicate tombstones)" as *u8, t4, ctr) 194 195 // ---- T5 NEG-CONTROL: a claim that never existed. The reap must REFUSE, and must not create it. 196 let d_abs: i64 = jr_reap_one(JG_FIX, old_id - JG_OFF_ABSENT, now, maxage, 0, info, pbuf, JR_PATH, sc, line, JR_LINE) 197 jr_mkpath(qbuf, JR_PATH, JG_FIX, old_id - JG_OFF_ABSENT, JR_EXT_CLAIM) 198 let abs_after: i64 = jg_size(qbuf, sc) 199 var t5: i64 = 0 200 if d_abs == JR_D_ABSENT { if abs_after < 0 { t5 = 1 } } 201 gv_puts(" absent id: " as *u8); gv_puts(jr_decision_name(d_abs)) 202 gv_puts(" size_after=" as *u8); gv_num(abs_after); gv_puts("\n" as *u8) 203 gv_check("T5 neg-control-reaping-a-claim-that-never-existed-REFUSES-and-creates-nothing" as *u8, t5, ctr) 204 205 // ---- T6 NEG-CONTROL: a DONE claim is healthy history, however old. Age alone must not condemn it. 206 jr_mkpath(qbuf, JR_PATH, JG_FIX, old_id - JG_OFF_DONE, JR_EXT_CLAIM) 207 let dn_before: i64 = jg_size(qbuf, sc) 208 let d_done: i64 = jr_reap_one(JG_FIX, old_id - JG_OFF_DONE, now, maxage, 0, info, pbuf, JR_PATH, sc, line, JR_LINE) 209 let dn_after: i64 = jg_size(qbuf, sc) 210 var t6: i64 = 0 211 if d_done == JR_D_DONE { if dn_after == dn_before { t6 = 1 } } 212 gv_check("T6 neg-control-an-old-DONE-claim-is-never-reaped-and-is-left-byte-unchanged" as *u8, t6, ctr) 213 214 // ---- T7 NEG-CONTROL: ORPHAN has a DIFFERENT remedy. The result exists; burying the claim would 215 // destroy the signal that the output is there to be published. 216 jr_mkpath(qbuf, JR_PATH, JG_FIX, old_id - JG_OFF_HASOUT, JR_EXT_CLAIM) 217 let or_before: i64 = jg_size(qbuf, sc) 218 let d_orph: i64 = jr_reap_one(JG_FIX, old_id - JG_OFF_HASOUT, now, maxage, 0, info, pbuf, JR_PATH, sc, line, JR_LINE) 219 let or_after: i64 = jg_size(qbuf, sc) 220 var t7: i64 = 0 221 if d_orph == JR_D_HAS_OUT { if or_after == or_before { t7 = 1 } } 222 gv_check("T7 neg-control-an-old-claim-with-a-sibling-.out-is-never-reaped (ORPHAN, a different remedy)" as *u8, t7, ctr) 223 224 // ---- T8 NEG-CONTROL: UNKNOWN IS ITS OWN BUCKET on the WRITE side too. A claim we cannot classify 225 // is never reaped on a guess. 226 jr_mkpath(qbuf, JR_PATH, JG_FIX, old_id - JG_OFF_GARBAGE, JR_EXT_CLAIM) 227 let gb_before: i64 = jg_size(qbuf, sc) 228 let d_gbg: i64 = jr_reap_one(JG_FIX, old_id - JG_OFF_GARBAGE, now, maxage, 0, info, pbuf, JR_PATH, sc, line, JR_LINE) 229 let gb_after: i64 = jg_size(qbuf, sc) 230 var t8: i64 = 0 231 if d_gbg == JR_D_UNPARSED { if gb_after == gb_before { t8 = 1 } } 232 gv_check("T8 neg-control-a-claim-matching-no-known-marker-is-never-reaped (UNKNOWN is its own bucket)" as *u8, t8, ctr) 233 234 // ---- T9 THE OTHER REAL REPRESENTATION. Three of the four live dead claims carry state=CLAIMED; 235 // the fourth is 0 bytes, because that is what a pre-2026-08-07 reservation looks like. Both must 236 // reap, or the fix covers three quarters of the population it was built for. 237 jr_mkpath(qbuf, JR_PATH, JG_FIX, old_id - JG_OFF_EMPTYCLM, JR_EXT_CLAIM) 238 let em_before: i64 = jg_size(qbuf, sc) 239 let d_emp: i64 = jr_reap_one(JG_FIX, old_id - JG_OFF_EMPTYCLM, now, maxage, 0, info, pbuf, JR_PATH, sc, line, JR_LINE) 240 let em_after: i64 = jg_size(qbuf, sc) 241 let em_state: i64 = jr_state_of_path(qbuf, sc) 242 var t9: i64 = 0 243 if em_before == 0 { if d_emp == JR_D_REAP { if em_after > 0 { if em_state == JR_ST_REAPED { t9 = 1 } } } } 244 gv_check("T9 a 0-byte claim (the pre-fix representation) also reaps, and reads back REAPED" as *u8, t9, ctr) 245 246 // ---- T10 BLAST RADIUS, with a POSITIVE CONTROL. A guard that refuses everything passes every 247 // negative test, so the allowed cases are asserted in the same tooth as the refused ones. 248 var t10: i64 = 0 249 let ok_jobs: i64 = jr_dir_allowed(JR_JOBS) 250 let ok_tmp: i64 = jr_dir_allowed(JG_FIX) 251 let no_know: i64 = jr_dir_allowed("knowledge/store" as *u8) 252 let no_root: i64 = jr_dir_allowed("/etc" as *u8) 253 let no_trav: i64 = jr_dir_allowed("/tmp/../etc" as *u8) 254 let no_empty:i64 = jr_dir_allowed("" as *u8) 255 if ok_jobs == 1 { if ok_tmp == 1 { if no_know == 0 { if no_root == 0 { if no_trav == 0 { if no_empty == 0 { t10 = 1 } } } } } } 256 gv_puts(" dir guard: _jobs=" as *u8); gv_num(ok_jobs) 257 gv_puts(" /tmp fixture=" as *u8); gv_num(ok_tmp) 258 gv_puts(" knowledge/store=" as *u8); gv_num(no_know) 259 gv_puts(" /etc=" as *u8); gv_num(no_root) 260 gv_puts(" traversal=" as *u8); gv_num(no_trav) 261 gv_puts(" empty=" as *u8); gv_num(no_empty); gv_puts("\n" as *u8) 262 gv_check("T10 neg-control-the-dir-guard-refuses-/etc-traversal-and-knowledge-while-ALLOWING-_jobs-and-/tmp" as *u8, t10, ctr) 263 264 // ---- T11 the DRY RUN decides identically and writes nothing -- otherwise `check` is a different 265 // tool from `reap` and proves nothing about it. 266 jr_mkpath(qbuf, JR_PATH, JG_FIX, old_id - JG_OFF_ABSENT, JR_EXT_CLAIM) 267 sys_unlinkat(qbuf) 268 let dry_id: i64 = old_id - JG_OFF_ABSENT 269 jg_plant(pbuf, dry_id, JG_CLAIMED) 270 let dr_before: i64 = jg_size(qbuf, sc) 271 let d_dry: i64 = jr_reap_one(JG_FIX, dry_id, now, maxage, 1, info, pbuf, JR_PATH, sc, line, JR_LINE) 272 let dr_after: i64 = jg_size(qbuf, sc) 273 var t11: i64 = 0 274 if d_dry == JR_D_REAP { if dr_after == dr_before { if dr_before > 0 { t11 = 1 } } } 275 gv_check("T11 a DRY run reaches the same REAP decision and leaves the claim byte-unchanged" as *u8, t11, ctr) 276 277 // ---- T12 the shared threshold is a positive number and its SOURCE is stated, so nobody has to 278 // guess which bar was applied. A threshold nobody can see is the magic-number defect wearing a conf. 279 var t12: i64 = 0 280 if maxage > 0 { if looked == 1 { t12 = 1 } } 281 gv_check("T12 the shared age threshold is positive and its source (conf or code default) is printed" as *u8, t12, ctr) 282 283 return gv_verdict("NX-JOBCLAIM-REAP" as *u8, ctr, 284 "the tombstone is APPENDED and the classifier tests REAPED first, so the reader cannot read a reaped claim as still pending" as *u8) 285}