code wiki / (root) / nx_segrace_gate.nx

nx_segrace_gate.nx source

↩ module page · 287 lines · 13081 B

1// nx_segrace_gate.nx -- does ss_commit lose whole SEGMENTS under concurrency? 2// 3// nx_sts_lock_gate proved the sts_ family loses ROWS without <prefix>plock. This gate goes one 4// layer DOWN, to the act every writer in the fleet performs: ss_commit. Both sts_seed and 5// sts_append_fast end in ss_commit(prefix, w, ss_next_segid(prefix)), and so do 261 ss_ callers. 6// If ss_commit is itself an unlocked read-modify-write, then NO lock above it can help, because 7// the two writer families do not share one. 8// 9// TWO LOSSES ARE POSSIBLE AND THEY ARE DIFFERENT: 10// (a) SEGID COLLISION -- ss_next_segid is an unlocked max+1 scan, so two writers pick the SAME 11// id and ss_write_seg has one segment file overwrite the other. Bytes destroyed on disk. 12// (b) MANIFEST LOST UPDATE -- ss_commit reads manifest.txt, appends one seg- line, writes the 13// temp and renames. The rename is atomic, so the manifest is never TORN -- which is exactly 14// what disguises the lost update. Last renamer wins and the other writer's seg- line is gone; 15// its segment file still exists on disk but no reader can ever see it. 16// 17// THE CONTROL IS DELIBERATELY A HAND COPY, AND THAT IS A HAZARD I AM CLOSING ON PURPOSE. 18// g_commit_unlocked below is the CURRENT ss_commit body copied VERBATIM minus the lock. A 19// hand-copied differential probe is evidence ONLY if it is faithful -- a copy that silently fixes 20// the bug exonerates the guilty. So the acceptance rule is two-phase: 21// PRE-FIX : arm U and arm R must BOTH lose, and lose comparably. That is what proves the copy 22// is faithful, because it is being raced against the very code it was copied from. 23// POST-FIX : arm U still loses, arm R loses NOTHING. The lock is then the only variable. 24// Running only the second phase would let an unfaithful copy pass as proof. 25// 26// NON-VACUITY: no artificial sleep is injected. The window is the real one -- ss_begin_cap + ss_add 27// between the segid read and the commit, and the readall->rename span inside the commit. If real 28// contention does not reproduce the loss the gate reports VACUOUS, never GREEN. 29// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0 30import "nx_seg_store.nx" 31import "nx_syscalls.nx" 32import "nx_gate_verdict.nx" 33 34const G_W: i64 = 6 // concurrent writer processes 35const G_R: i64 = 4 // commits each writer performs (deadline-bounded: the locked arm serialises) 36const G_KEYCAP: i64 = 64 37const G_SEGCAP: i64 = 65536 38const G_KIND_LIVE: i64 = 1 39const G_FOLDS: i64 = 6 // compaction rounds raced against the writers in T4 40 41func g_p(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 42func g_num(v: i64) -> i64 { 43 if v == 0 { g_p("0" as *u8); return 0 } 44 var x: i64 = v 45 if x < 0 { g_p("-" as *u8); x = 0 - x } 46 let b: *u8 = sys_mmap(32) 47 var i: i64 = 0 48 while x > 0 { b[i] = ((x % 10) + 48) as u8; x = x / 10; i = i + 1 } 49 while i > 0 { i = i - 1; sys_write(1, ((b as i64) + i) as *u8, 1) } 50 return 0 51} 52 53// every writer/row pair gets its OWN key, so a survivor count is an exact census of what was kept 54func g_key(out: *u8, wid: i64, r: i64) -> i64 { 55 var o: i64 = ss_cat(out, 0, "k" as *u8) 56 o = ss_catn(out, o, wid) 57 o = ss_cat(out, o, "_" as *u8) 58 o = ss_catn(out, o, r) 59 out[o] = 0 as u8 60 return o 61} 62 63// ---- NEGATIVE CONTROL: nx_seg_store.nx ss_commit body, VERBATIM, minus the plane lock ---------- 64func g_commit_unlocked(prefix: *u8, w: *i64, segid: i64) -> i64 { 65 let wrc: i64 = ss_write_seg(prefix, w, segid) 66 if wrc != 0 { return wrc } 67 let mf: *u8 = sys_mmap(512) 68 let mt: *u8 = sys_mmap(512) 69 var o: i64 = 0 70 o = ss_cat(mf, o, prefix) 71 o = ss_cat(mf, o, "manifest.txt" as *u8) 72 mf[o] = 0 as u8 73 o = 0 74 o = ss_cat(mt, o, prefix) 75 o = ss_cat(mt, o, "manifest.tmp" as *u8) 76 mt[o] = 0 as u8 77 let szp: *i64 = sys_mmap(16) as *i64 78 let old: *u8 = ss_readall(mf, szp) 79 var osz: i64 = szp[0] 80 if osz < 0 { osz = 0 } 81 let nb: *u8 = sys_mmap(osz + 128) 82 var no: i64 = 0 83 var t: i64 = 0 84 while t < osz { nb[no] = old[t]; no = no + 1; t = t + 1 } 85 no = ss_cat(nb, no, "seg-" as *u8) 86 no = ss_catn(nb, no, segid) 87 nb[no] = 10 as u8 88 no = no + 1 89 if ss_writefile(mt, nb, no) != 0 { return 0 - 3 } 90 if sys_renameat(mt, mf) != 0 { return 0 - 4 } 91 ss_syncdir(prefix) 92 return 0 93} 94 95// truncate the live manifest -> every prior segment is unreferenced, so each arm starts empty. 96// ADDITIVE: the segment files themselves are left on disk untouched (rule 13). 97func g_reset(prefix: *u8) -> i64 { 98 let mf: *u8 = sys_mmap(512) 99 var o: i64 = ss_cat(mf, 0, prefix) 100 o = ss_cat(mf, o, "manifest.txt" as *u8) 101 mf[o] = 0 as u8 102 let e: *u8 = sys_mmap(16) 103 ss_writefile(mf, e, 0) 104 return 0 105} 106 107// mode 1 = the real ss_commit, mode 0 = the verbatim unlocked control. NOTHING else differs. 108func g_worker(prefix: *u8, wid: i64, mode: i64) -> i64 { 109 var r: i64 = 0 110 while r < G_R { 111 let key: *u8 = sys_mmap(G_KEYCAP) 112 g_key(key, wid, r) 113 let wr: *i64 = ss_begin_cap(G_SEGCAP) 114 ss_add(wr, G_KIND_LIVE, key, "v" as *u8, 1) 115 let segid: i64 = ss_next_segid(prefix) 116 if mode == 1 { ss_commit(prefix, wr, segid) } else { g_commit_unlocked(prefix, wr, segid) } 117 r = r + 1 118 } 119 return 0 120} 121 122func g_survivors(prefix: *u8) -> i64 { 123 let pp: *i64 = sys_mmap(16) as *i64 124 let lp: *i64 = sys_mmap(16) as *i64 125 var found: i64 = 0 126 var wid: i64 = 0 127 while wid < G_W { 128 var r: i64 = 0 129 while r < G_R { 130 let key: *u8 = sys_mmap(G_KEYCAP) 131 g_key(key, wid, r) 132 if ss_get(prefix, key, pp, lp) == 1 { found = found + 1 } 133 r = r + 1 134 } 135 wid = wid + 1 136 } 137 return found 138} 139 140func g_arm(prefix: *u8, mode: i64) -> i64 { 141 g_reset(prefix) 142 var k: i64 = 0 143 while k < G_W { 144 let pid: i64 = sys_fork() 145 if pid == 0 { 146 g_worker(prefix, k, mode) 147 sys_exit_group(0) 148 } 149 k = k + 1 150 } 151 let st: *i64 = sys_mmap(16) as *i64 152 var reaped: i64 = 0 153 while reaped < G_W { if sys_wait4(0 - 1, st, 0) > 0 { reaped = reaped + 1 } else { reaped = G_W } } 154 return g_survivors(prefix) 155} 156 157// ---- T4: WRITERS RACING THE COMPACTOR ----------------------------------------------------------- 158// ss_commit appending a segment is only half the story. ss_compact REPLACES the manifest with one 159// naming ONLY the merged segment, so pre-fix any commit that landed between its listing and its swap 160// was erased -- a lost SEGMENT, not a lost row. This is the nastier defect because the compactor runs 161// unattended on a standing beat (nx_store_fold_beat) and, via nx_store_query, on the QUERY path: a 162// read could delete a concurrent write. 163// Returns the number of folds that SUCCEEDED (ss_compact returns the merged segid, >0, on success). 164// That count -- not the leftover segment total -- is the honest non-vacuity signal: see g_arm_compact. 165func g_compactor(prefix: *u8) -> i64 { 166 var folds: i64 = 0 167 var i: i64 = 0 168 while i < G_FOLDS { 169 if ss_compact(prefix, ss_next_segid(prefix)) > 0 { folds = folds + 1 } 170 i = i + 1 171 } 172 return folds 173} 174 175// live = the segment count left in the manifest, the NON-VACUITY probe: if the compactor never 176// actually folded, this arm raced nothing and must not be read as evidence of safety. 177func g_arm_compact(prefix: *u8, live: *i64) -> i64 { 178 g_reset(prefix) 179 let w0: *i64 = ss_begin_cap(4096) 180 ss_add(w0, G_KIND_LIVE, "seedk" as *u8, "v" as *u8, 1) 181 ss_commit(prefix, w0, 0) 182 var k: i64 = 0 183 while k < G_W { 184 let pid: i64 = sys_fork() 185 if pid == 0 { 186 g_worker(prefix, k, 1) 187 sys_exit_group(0) 188 } 189 k = k + 1 190 } 191 let cpid: i64 = sys_fork() 192 if cpid == 0 { 193 sys_exit_group(g_compactor(prefix)) 194 } 195 let st: *i64 = sys_mmap(16) as *i64 196 // Reap the COMPACTOR SPECIFICALLY so its exit code (the successful-fold count) is not lost in a 197 // wait-for-anyone loop. THIS IS THE NON-VACUITY FIX: the old probe inferred "the compactor never 198 // folded" from the LEFTOVER SEGMENT COUNT, which is a bad proxy -- a compactor that folds all its 199 // rounds EARLY, before the writers commit, leaves a high segment count and was reported VACUOUS 200 // despite having done exactly what it was asked. Ask the compactor what it did instead. 201 st[0] = 0 202 sys_wait4(cpid, st, 0) 203 live[1] = (st[0] / 256) % 256 204 var reaped: i64 = 0 205 while reaped < G_W { if sys_wait4(0 - 1, st, 0) > 0 { reaped = reaped + 1 } else { reaped = G_W } } 206 let sp: *i64 = sys_mmap(8) as *i64 207 live[0] = ss_manifest_dyn(prefix, sp) 208 return g_survivors(prefix) 209} 210 211func main(argc: i64, argv: *i64) -> i64 { 212 g_p("nx_segrace_gate -- does ss_commit lose segments under concurrency?\n\n" as *u8) 213 // HERMETIC FIXTURE (2026-08-07). These planes used to live in knowledge/store/ -- the PRODUCTION 214 // store that the nx_segguard beat sweeps every 600s. MEASURED 2026-08-07T10:01:56: segguard folded 215 // `segracegate-real 23 -> 1` WHILE this gate was mid-run, and the gate then reported "COMPACTION 216 // STILL ERASES COMMITTED SEGMENTS -- lost 1". That RED is unattributable: a third party rewrote the 217 // fixture under the code being measured, so the gate cannot tell its subject from its environment. 218 // Its sibling _ss_compact_cap_gate banked this exact lesson ("Scratch lives in /tmp/ccgate, NOT 219 // knowledge/store: the production store is swept by the 600s nx_segguard beat ... spurious RED 220 // before=299") and it was never carried across. 221 // ★A LESSON LEARNED IN ONE GATE AND NOT CARRIED TO ITS SIBLING IS A LESSON THE ESTATE DID NOT LEARN. 222 // ★A GATE THAT SHARES ITS FIXTURE WITH A PRODUCTION BEAT IS MEASURING THE BEAT. 223 sys_mkdir("/tmp/segracegate\x00" as *u8, 0x1ed) 224 let pu: *u8 = "/tmp/segracegate/segracegate-unlocked-" as *u8 225 let pr: *u8 = "/tmp/segracegate/segracegate-real-" as *u8 226 let want: i64 = G_W * G_R 227 228 let got_u: i64 = g_arm(pu, 0) 229 g_p(" T1 UNLOCKED control (verbatim pre-fix ss_commit body): " as *u8) 230 g_num(got_u); g_p(" of " as *u8); g_num(want); g_p(" keys survived\n" as *u8) 231 232 let got_r: i64 = g_arm(pr, 1) 233 g_p(" T2 REAL ss_commit : " as *u8) 234 g_num(got_r); g_p(" of " as *u8); g_num(want); g_p(" keys survived\n\n" as *u8) 235 236 // NON-VACUITY FIRST: a concurrency gate whose race never fires proves nothing. 237 var vac: i64 = 0 238 if got_u >= want { vac = 1 } 239 if vac == 1 { 240 g_p(" VACUOUS -- the unlocked control lost NOTHING, so this run raced nothing.\n" as *u8) 241 g_p(" Raise G_W/G_R or widen the window; do NOT read this as evidence of safety.\n" as *u8) 242 } 243 244 var pass: i64 = 0 245 if vac == 0 { 246 if got_r == want { 247 pass = 1 248 g_p(" T3 ss_commit IS SERIALISED: unlocked control lost " as *u8); g_num(want - got_u) 249 g_p(", real ss_commit lost 0.\n" as *u8) 250 } else { 251 g_p(" T3 ss_commit IS NOT SERIALISED: it lost " as *u8); g_num(want - got_r) 252 g_p(" of " as *u8); g_num(want) 253 g_p(" keys. Segment files exist on disk but the manifest no longer names them.\n" as *u8) 254 g_p(" Both arms losing is the EXPECTED pre-fix result and is what proves the\n" as *u8) 255 g_p(" hand-copied control faithful -- it is racing the code it was copied from.\n" as *u8) 256 } 257 } 258 259 // T4 -- the compactor arm. Reported HONESTLY: it demonstrates the fixed behaviour and will go RED 260 // if the compaction lock is ever removed, but unlike T1/T2 it carries no negative control in the 261 // same run, so its "would have failed before" rests on the mechanism plus the T1 result. 262 let foldp: *i64 = sys_mmap(16) as *i64 263 let pc: *u8 = "/tmp/segracegate/segracegate-compact-" as *u8 264 let got_c: i64 = g_arm_compact(pc, foldp) 265 g_p(" T4 writers racing ss_compact : " as *u8) 266 g_num(got_c); g_p(" of " as *u8); g_num(want) 267 g_p(" keys survived, live segments left = " as *u8); g_num(foldp[0]) 268 g_p(", folds that SUCCEEDED = " as *u8); g_num(foldp[1]); g_p("\n" as *u8) 269 if got_c < want { 270 pass = 0 271 g_p(" COMPACTION STILL ERASES COMMITTED SEGMENTS -- lost " as *u8) 272 g_num(want - got_c); g_p("\n" as *u8) 273 } 274 if foldp[1] == 0 { 275 pass = 0 276 g_p(" VACUOUS: ZERO folds succeeded, so T4 raced nothing. Measured from the compactor's\n" as *u8) 277 g_p(" own exit code, not from the leftover segment count -- a compactor that folds early\n" as *u8) 278 g_p(" leaves many segments and was previously misreported as vacuous.\n" as *u8) 279 } 280 281 let ctr: *i64 = gv_ctr() 282 ctr[0] = pass 283 ctr[1] = 1 284 let rc: i64 = gv_verdict("SEGRACE-GATE" as *u8, ctr, "ss_commit serialises concurrent writers" as *u8) 285 sys_exit(rc) 286 return rc 287}