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}