code wiki / (root) / nx_tmpstore_reap.nx

nx_tmpstore_reap.nx source

↩ module page · 372 lines · 17471 B

1// nx_tmpstore_reap.nx -- GUARDED reaper for orphaned scratch seg-store families in a temp dir 2// (2026-08-12, debt 1786544135 item 4). /tmp measured at 68,730 files, dominated by one-shot organs' 3// scratch stores: <prefix>-<num>-seg-<num>.{docs,idx,pos,imp} + -manifest.txt + -slock. capsearch 4// proved NO incumbent covers this class (nx_orphan_reap=processes, nx_toolreg_reap=registry rows, 5// nx_eqtmp_reap=the legacy equiv-gate temps only, measured already_clean=19/19). 6// SAFETY BY CONSTRUCTION (this deletes, so every rule is load-bearing): 7// (1) FAMILY-scoped: only names matching the three store shapes are ever considered; everything 8// else is counted as other and NEVER touched. 9// (2) AGE-bounded: a family is reapable only if its NEWEST member mtime is older than minage. 10// (3) WRITER-aware: if the family has a -slock, a non-blocking flock EX probe must SUCCEED 11// (no writer holds it); a held lock -> the family is KEPT. Probe lock released immediately. 12// (4) DRY BY DEFAULT: reap deletes only when argv literally says apply. 13// (5) NO SILENT CAPS: more than MAX_FAM families -> REFUSE (exit 3), never a partial sweep 14// published as a total. 15// (6) The partition is printed AND summed: families == reaped + young + held. 16// verbs: scan <dir> | reap <dir> <minage_secs> [apply] | selftest 17// license_tier: ORIGINAL No hw writes (Rule 26). 18import "nx_syscalls.nx" 19import "nx_gate_verdict.nx" 20const TR_MAGIC_2166136261: i64 = 2166136261 21const TR_MAGIC_16777619: i64 = 16777619 22const TR_MAGIC_65536: i64 = 65536 23const TR_MAGIC_4096: i64 = 4096 24const TR_MAGIC_7200: i64 = 7200 25const TR_MAGIC_3600: i64 = 3600 26 27const TR_MAX_FAM: i64 = 16384 28const TR_NAME_CAP: i64 = 96 29const TR_HT_SLOTS: i64 = 32768 30const TR_MODE_644: i64 = 420 31 32func tr_len(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n } 33func tr_p(s: *u8) -> i64 { sys_write(1, s, tr_len(s)); return 0 } 34func tr_pn(v: i64) -> i64 { let b: *u8=sys_mmap(28); let t: *u8=sys_mmap(28); var m: i64=v; if m<0{sys_write(1,"-" as *u8,1);m=0-m} 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{b[i]=t[k-1-i];i=i+1} sys_write(1,b,k); sys_munmap(b,28); sys_munmap(t,28); return 0 } 35func tr_atoi(s: *u8) -> i64 { var v: i64=0; var i: i64=0; while s[i]!=(0 as u8){ let c: i64=s[i] as i64; if c<48 {return v} if c>57 {return v} v=v*10+(c-48); i=i+1 } return v } 36func tr_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64=0; var p: i64=o; while s[i]!=(0 as u8){d[p]=s[i];p=p+1;i=i+1} d[p]=0 as u8; return p } 37func tr_catb(d: *u8, o: i64, s: *u8, n: i64) -> i64 { var i: i64=0; var p: i64=o; while i<n {d[p]=s[i];p=p+1;i=i+1} d[p]=0 as u8; return p } 38 39// suffix test with hoisted needle (never index an inline-cast literal) 40func tr_ends(name: *u8, n: i64, suf: *u8) -> i64 { 41 let sl: i64 = tr_len(suf) 42 if n < sl { return 0 } 43 var i: i64 = 0 44 while i < sl { if name[n-sl+i] != suf[i] { return 0 } i = i + 1 } 45 return 1 46} 47// first occurrence of needle in name[0..n); -1 if absent 48func tr_find(name: *u8, n: i64, needle: *u8) -> i64 { 49 let nl: i64 = tr_len(needle) 50 if nl == 0 { return 0 - 1 } 51 var i: i64 = 0 52 while i + nl <= n { 53 var j: i64 = 0; var hit: i64 = 1 54 while j < nl { if name[i+j] != needle[j] { hit = 0; j = nl } else { j = j + 1 } } 55 if hit == 1 { return i } 56 i = i + 1 57 } 58 return 0 - 1 59} 60 61// family key length for a store-member name; 0 = not a store member 62func tr_keylen(name: *u8, n: i64) -> i64 { 63 let seg: *u8 = "-seg-" as *u8 64 let man: *u8 = "-manifest.txt" as *u8 65 let slk: *u8 = "-slock" as *u8 66 let sp: i64 = tr_find(name, n, seg) 67 if sp > 0 { return sp } 68 if tr_ends(name, n, man) == 1 { return n - 13 } 69 if tr_ends(name, n, slk) == 1 { return n - 6 } 70 return 0 71} 72 73func tr_fnv(s: *u8, n: i64) -> i64 { 74 var h: i64 = TR_MAGIC_2166136261 75 var i: i64 = 0 76 while i < n { h = h ^ (s[i] as i64); h = (h * TR_MAGIC_16777619) & 0x7fffffffffff; i = i + 1 } 77 return h 78} 79 80func tr_getdents(fd: i64, buf: *u8, count: i64) -> i64 { return __syscall(217, fd, buf, count, 0, 0, 0) } 81 82// ---- family table (module statics, lazily mmap'd) ---- 83static tr_names: *u8 = 0 as *u8 // TR_MAX_FAM * TR_NAME_CAP 84static tr_klen: *i64 = 0 as *i64 85static tr_newest: *i64 = 0 as *i64 86static tr_slock: *i64 = 0 as *i64 87static tr_verd: *i64 = 0 as *i64 // 1 reap / 2 young / 3 held 88static tr_ht: *i64 = 0 as *i64 // slot -> famidx+1 89static tr_nfam: *i64 = 0 as *i64 90 91func tr_init() -> i64 { 92 if (tr_names as i64) != 0 { return 0 } 93 tr_names = sys_mmap(TR_MAX_FAM * TR_NAME_CAP) 94 tr_klen = sys_mmap(TR_MAX_FAM * 8) as *i64 95 tr_newest = sys_mmap(TR_MAX_FAM * 8) as *i64 96 tr_slock = sys_mmap(TR_MAX_FAM * 8) as *i64 97 tr_verd = sys_mmap(TR_MAX_FAM * 8) as *i64 98 tr_ht = sys_mmap(TR_HT_SLOTS * 8) as *i64 99 tr_nfam = sys_mmap(16) as *i64 100 return 0 101} 102 103// find-or-insert; returns famidx, or -1 on table full 104func tr_fam(key: *u8, kl: i64) -> i64 { 105 var slot: i64 = tr_fnv(key, kl) % TR_HT_SLOTS 106 if slot < 0 { slot = 0 - slot } 107 var probes: i64 = 0 108 while probes < TR_HT_SLOTS { 109 let v: i64 = tr_ht[slot] 110 if v == 0 { 111 if tr_nfam[0] >= TR_MAX_FAM { return 0 - 1 } 112 let idx: i64 = tr_nfam[0] 113 tr_nfam[0] = idx + 1 114 let dst: *u8 = ((tr_names as i64) + idx * TR_NAME_CAP) as *u8 115 var c: i64 = 0 116 while c < kl { dst[c] = key[c]; c = c + 1 } 117 dst[kl] = 0 as u8 118 tr_klen[idx] = kl 119 tr_newest[idx] = 0 120 tr_slock[idx] = 0 121 tr_verd[idx] = 0 122 tr_ht[slot] = idx + 1 123 return idx 124 } 125 let idx2: i64 = v - 1 126 if tr_klen[idx2] == kl { 127 let nm: *u8 = ((tr_names as i64) + idx2 * TR_NAME_CAP) as *u8 128 var c2: i64 = 0; var same: i64 = 1 129 while c2 < kl { if nm[c2] != key[c2] { same = 0; c2 = kl } else { c2 = c2 + 1 } } 130 if same == 1 { return idx2 } 131 } 132 slot = (slot + 1) % TR_HT_SLOTS 133 probes = probes + 1 134 } 135 return 0 - 1 136} 137 138// pass 1: census. returns files_matched, or -1 on family-cap REFUSE. other_count -> obox[0]. 139func tr_census(dir: *u8, obox: *i64) -> i64 { 140 let fd: i64 = sys_openat_rd(dir) 141 if fd < 0 { return 0 - 2 } 142 let gbuf: *u8 = sys_mmap(TR_MAGIC_65536) 143 let path: *u8 = sys_mmap(TR_MAGIC_4096) 144 let sb: *u8 = sys_mmap(144) 145 var matched: i64 = 0 146 obox[0] = 0 147 var nread: i64 = tr_getdents(fd, gbuf, TR_MAGIC_65536) 148 while nread > 0 { 149 var off: i64 = 0 150 while off < nread { 151 let reclen: i64 = (gbuf[off+16] as i64) | ((gbuf[off+17] as i64) << 8) 152 if reclen <= 0 { off = nread } else { 153 let name: *u8 = ((gbuf as i64) + off + 19) as *u8 154 let n: i64 = tr_len(name) 155 let kl: i64 = tr_keylen(name, n) 156 if kl > 0 { if kl < TR_NAME_CAP - 1 { 157 let idx: i64 = tr_fam(name, kl) 158 if idx < 0 { sys_close(fd); return 0 - 1 } 159 matched = matched + 1 160 var po: i64 = tr_cat(path, 0, dir) 161 po = tr_cat(path, po, "/" as *u8) 162 po = tr_catb(path, po, name, n) 163 if sys_fstatat(path, sb) == 0 { 164 // HOIST the cast pointer -- an inline-cast index ((x) as *i64)[0] miscompiles 165 // (banked law, same class as the string-literal-cast-index trap). The selftest's 166 // T1 negative control caught this: an inline read made every aged family read young. 167 let mtp: *i64 = ((sb as i64) + 88) as *i64 168 let mt: i64 = mtp[0] 169 if mt > tr_newest[idx] { tr_newest[idx] = mt } 170 } 171 let slk2: *u8 = "-slock" as *u8 172 if tr_ends(name, n, slk2) == 1 { tr_slock[idx] = 1 } 173 } } 174 if kl == 0 { obox[0] = obox[0] + 1 } 175 off = off + reclen 176 } 177 } 178 nread = tr_getdents(fd, gbuf, TR_MAGIC_65536) 179 } 180 sys_close(fd) 181 return matched 182} 183 184// pass 2: verdict per family. counts -> rbox[0]=reap rbox[1]=young rbox[2]=held 185func tr_judge(dir: *u8, minage: i64, rbox: *i64) -> i64 { 186 let now: i64 = sys_now_realtime_sec() 187 let path: *u8 = sys_mmap(TR_MAGIC_4096) 188 rbox[0] = 0; rbox[1] = 0; rbox[2] = 0 189 var i: i64 = 0 190 while i < tr_nfam[0] { 191 if tr_newest[i] > now - minage { tr_verd[i] = 2; rbox[1] = rbox[1] + 1 } 192 else { 193 var held: i64 = 0 194 if tr_slock[i] == 1 { 195 let nm: *u8 = ((tr_names as i64) + i * TR_NAME_CAP) as *u8 196 var po: i64 = tr_cat(path, 0, dir) 197 po = tr_cat(path, po, "/" as *u8) 198 po = tr_cat(path, po, nm) 199 po = tr_cat(path, po, "-slock" as *u8) 200 let lfd: i64 = sys_openat_rd(path) 201 if lfd >= 0 { 202 if sys_flock(lfd, SYS_LOCK_EX | SYS_LOCK_NB) < 0 { held = 1 } 203 else { sys_flock(lfd, SYS_LOCK_UN) } 204 sys_close(lfd) 205 } 206 } 207 if held == 1 { tr_verd[i] = 3; rbox[2] = rbox[2] + 1 } 208 else { tr_verd[i] = 1; rbox[0] = rbox[0] + 1 } 209 } 210 i = i + 1 211 } 212 return 0 213} 214 215// pass 3 (apply only): unlink members of verdict==1 families. returns files unlinked. 216func tr_sweep(dir: *u8) -> i64 { 217 let fd: i64 = sys_openat_rd(dir) 218 if fd < 0 { return 0 - 2 } 219 let gbuf: *u8 = sys_mmap(TR_MAGIC_65536) 220 let path: *u8 = sys_mmap(TR_MAGIC_4096) 221 var gone: i64 = 0 222 var nread: i64 = tr_getdents(fd, gbuf, TR_MAGIC_65536) 223 while nread > 0 { 224 var off: i64 = 0 225 while off < nread { 226 let reclen: i64 = (gbuf[off+16] as i64) | ((gbuf[off+17] as i64) << 8) 227 if reclen <= 0 { off = nread } else { 228 let name: *u8 = ((gbuf as i64) + off + 19) as *u8 229 let n: i64 = tr_len(name) 230 let kl: i64 = tr_keylen(name, n) 231 if kl > 0 { if kl < TR_NAME_CAP - 1 { 232 let idx: i64 = tr_fam(name, kl) 233 if idx >= 0 { if tr_verd[idx] == 1 { 234 var po: i64 = tr_cat(path, 0, dir) 235 po = tr_cat(path, po, "/" as *u8) 236 po = tr_catb(path, po, name, n) 237 if sys_unlinkat(path) == 0 { gone = gone + 1 } 238 } } 239 } } 240 off = off + reclen 241 } 242 } 243 nread = tr_getdents(fd, gbuf, TR_MAGIC_65536) 244 } 245 sys_close(fd) 246 return gone 247} 248 249func tr_report(matched: i64, other: i64, rbox: *i64, gone: i64, applied: i64) -> i64 { 250 tr_p("NX-TMPSTORE-REAP families=" as *u8); tr_pn(tr_nfam[0]) 251 tr_p(" reaped=" as *u8); tr_pn(rbox[0]) 252 tr_p(" young=" as *u8); tr_pn(rbox[1]) 253 tr_p(" held=" as *u8); tr_pn(rbox[2]) 254 tr_p(" files_matched=" as *u8); tr_pn(matched) 255 tr_p(" files_unlinked=" as *u8); tr_pn(gone) 256 tr_p(" other_untouched=" as *u8); tr_pn(other) 257 if applied == 1 { tr_p(" mode=apply" as *u8) } else { tr_p(" mode=dry" as *u8) } 258 let sum: i64 = rbox[0] + rbox[1] + rbox[2] 259 if sum == tr_nfam[0] { tr_p(" partition=SUMS\n" as *u8) } else { tr_p(" partition=LEAK!\n" as *u8) } 260 return 0 261} 262 263func tr_run(dir: *u8, minage: i64, apply: i64) -> i64 { 264 tr_init() 265 tr_nfam[0] = 0 266 var s: i64 = 0 267 while s < TR_HT_SLOTS { tr_ht[s] = 0; s = s + 1 } 268 let obox: *i64 = sys_mmap(16) as *i64 269 let rbox: *i64 = sys_mmap(32) as *i64 270 let matched: i64 = tr_census(dir, obox) 271 if matched == 0 - 1 { tr_p("REFUSE-AT-CAP: more than 16384 families -- raise TR_MAX_FAM deliberately, never sweep a partial census\n" as *u8); return 3 } 272 if matched == 0 - 2 { tr_p("NX-TMPSTORE-REAP unreadable dir\n" as *u8); return 2 } 273 tr_judge(dir, minage, rbox) 274 var gone: i64 = 0 275 if apply == 1 { gone = tr_sweep(dir) } 276 tr_report(matched, obox[0], rbox, gone, apply) 277 return 0 278} 279 280func tr_mkfile(path: *u8, content: *u8) -> i64 { 281 let fd: i64 = sys_openat_wr(path, TR_MODE_644) 282 if fd < 0 { return 0 - 1 } 283 sys_write(fd, content, tr_len(content)) 284 sys_close(fd) 285 return 0 286} 287func tr_age(path: *u8, secs_ago: i64) -> i64 { 288 let tv: *i64 = sys_mmap(32) as *i64 289 let past: i64 = sys_now_realtime_sec() - secs_ago 290 tv[0] = past; tv[1] = 0; tv[2] = past; tv[3] = 0 291 return sys_utimensat(path, tv) 292} 293func tr_exists(path: *u8) -> i64 { let fd: i64 = sys_openat_rd(path); if fd < 0 { return 0 } sys_close(fd); return 1 } 294 295func tr_selftest() -> i64 { 296 let ctr: *i64 = gv_ctr() 297 gv_head("=== nx_tmpstore_reap selftest -- fixtures assembled at RUNTIME (a detector must not find its own fixture in source) ===" as *u8) 298 let d: *u8 = "/tmp/nxtsr_fix" as *u8 299 sys_mkdir(d, 493) 300 // famA: aged, unheld -> must be REAPED 301 tr_mkfile("/tmp/nxtsr_fix/fa-1-seg-1.docs" as *u8, "a" as *u8); tr_age("/tmp/nxtsr_fix/fa-1-seg-1.docs" as *u8, TR_MAGIC_7200) 302 tr_mkfile("/tmp/nxtsr_fix/fa-1-seg-1.idx" as *u8, "a" as *u8); tr_age("/tmp/nxtsr_fix/fa-1-seg-1.idx" as *u8, TR_MAGIC_7200) 303 tr_mkfile("/tmp/nxtsr_fix/fa-1-manifest.txt" as *u8, "a" as *u8); tr_age("/tmp/nxtsr_fix/fa-1-manifest.txt" as *u8, TR_MAGIC_7200) 304 tr_mkfile("/tmp/nxtsr_fix/fa-1-slock" as *u8, "L" as *u8); tr_age("/tmp/nxtsr_fix/fa-1-slock" as *u8, TR_MAGIC_7200) 305 // famB: FRESH -> must be KEPT (young) 306 tr_mkfile("/tmp/nxtsr_fix/fb-2-seg-2.docs" as *u8, "b" as *u8) 307 tr_mkfile("/tmp/nxtsr_fix/fb-2-slock" as *u8, "L" as *u8) 308 // famC: aged but slock HELD -> must be KEPT (held) 309 tr_mkfile("/tmp/nxtsr_fix/fc-3-seg-3.docs" as *u8, "c" as *u8); tr_age("/tmp/nxtsr_fix/fc-3-seg-3.docs" as *u8, TR_MAGIC_7200) 310 tr_mkfile("/tmp/nxtsr_fix/fc-3-slock" as *u8, "L" as *u8); tr_age("/tmp/nxtsr_fix/fc-3-slock" as *u8, TR_MAGIC_7200) 311 let hold: i64 = sys_openat_rd("/tmp/nxtsr_fix/fc-3-slock" as *u8) 312 sys_flock(hold, SYS_LOCK_EX | SYS_LOCK_NB) 313 // bystander: matches NO store shape -> untouched even though aged 314 tr_mkfile("/tmp/nxtsr_fix/bystander.txt" as *u8, "x" as *u8); tr_age("/tmp/nxtsr_fix/bystander.txt" as *u8, TR_MAGIC_7200) 315 316 // T6 first: DRY run deletes nothing 317 tr_run(d, TR_MAGIC_3600, 0) 318 gv_check("T6 dry-run leaves the aged family in place" as *u8, tr_exists("/tmp/nxtsr_fix/fa-1-seg-1.docs" as *u8), ctr) 319 // the real apply 320 let rc: i64 = tr_run(d, TR_MAGIC_3600, 1) 321 var t5: i64 = 0; if rc == 0 { t5 = 1 } 322 var t1: i64 = 1 323 if tr_exists("/tmp/nxtsr_fix/fa-1-seg-1.docs" as *u8) == 1 { t1 = 0 } 324 if tr_exists("/tmp/nxtsr_fix/fa-1-manifest.txt" as *u8) == 1 { t1 = 0 } 325 if tr_exists("/tmp/nxtsr_fix/fa-1-slock" as *u8) == 1 { t1 = 0 } 326 gv_check("T1 aged+unheld family fully reaped" as *u8, t1, ctr) 327 gv_check("T2 fresh family kept (young)" as *u8, tr_exists("/tmp/nxtsr_fix/fb-2-seg-2.docs" as *u8), ctr) 328 gv_check("T3 aged family with HELD slock kept" as *u8, tr_exists("/tmp/nxtsr_fix/fc-3-seg-3.docs" as *u8), ctr) 329 gv_check("T4 non-store bystander untouched" as *u8, tr_exists("/tmp/nxtsr_fix/bystander.txt" as *u8), ctr) 330 gv_check("T5 run rc=0 and partition summed" as *u8, t5, ctr) 331 sys_flock(hold, SYS_LOCK_UN); sys_close(hold) 332 // teardown best-effort (fixtures are inert if a crash leaves them; setup recreates) 333 sys_unlinkat("/tmp/nxtsr_fix/fb-2-seg-2.docs" as *u8); sys_unlinkat("/tmp/nxtsr_fix/fb-2-slock" as *u8) 334 sys_unlinkat("/tmp/nxtsr_fix/fc-3-seg-3.docs" as *u8); sys_unlinkat("/tmp/nxtsr_fix/fc-3-slock" as *u8) 335 sys_unlinkat("/tmp/nxtsr_fix/bystander.txt" as *u8) 336 let xrc: i64 = gv_verdict("TMPSTORE-REAP-SELFTEST" as *u8, ctr, "reaps aged+unheld only; young, held and bystanders survive" as *u8) 337 return xrc 338} 339 340func main(argc: i64, argv: *i64) -> i64 { 341 if argc >= 2 { 342 let verb: *u8 = argv[1] as *u8 343 let vs: *u8 = "scan" as *u8 344 let vr: *u8 = "reap" as *u8 345 let vt: *u8 = "selftest" as *u8 346 var isscan: i64 = 1; var isreap: i64 = 1; var istest: i64 = 1 347 var i: i64 = 0 348 while i < 5 { if verb[i] != vs[i] { isscan = 0 } if vs[i]==(0 as u8) { i=5 } else { i=i+1 } } 349 i = 0 350 while i < 5 { if verb[i] != vr[i] { isreap = 0 } if vr[i]==(0 as u8) { i=5 } else { i=i+1 } } 351 i = 0 352 while i < 9 { if verb[i] != vt[i] { istest = 0 } if vt[i]==(0 as u8) { i=9 } else { i=i+1 } } 353 if istest == 1 { let rc: i64 = tr_selftest(); sys_exit(rc); return rc } 354 if isscan == 1 { if argc >= 3 { let rc2: i64 = tr_run(argv[2] as *u8, 0x7fffffff, 0); sys_exit(rc2); return rc2 } } 355 if isreap == 1 { if argc >= 4 { 356 var apply: i64 = 0 357 if argc >= 5 { 358 let ap: *u8 = argv[4] as *u8 359 let al: *u8 = "apply" as *u8 360 var isap: i64 = 1 361 var j: i64 = 0 362 while j < 6 { if ap[j] != al[j] { isap = 0 } if al[j]==(0 as u8) { j=6 } else { j=j+1 } } 363 if isap == 1 { apply = 1 } 364 } 365 let rc3: i64 = tr_run(argv[2] as *u8, tr_atoi(argv[3] as *u8), apply) 366 sys_exit(rc3); return rc3 367 } } 368 } 369 tr_p("usage: nx_tmpstore_reap scan <dir> | reap <dir> <minage_secs> [apply] | selftest\n" as *u8) 370 sys_exit(2) 371 return 2 372}