code wiki / _hdl_build / nx_docportal_search_sticky_gate.nx

nx_docportal_search_sticky_gate.nx source

↩ module page · 219 lines · 14584 B

1// nx_docportal_search_sticky_gate.nx -- does the web-shard cache KEEP its working handle when a reopen 2// comes back EMPTY, and does it refuse to MEMOISE the signature of that empty open? 3// 4// SUBJECT: nx_docportal_search_seg.nx dss_open_maybe_cached, the 2026-08-22 REFUSE-BEFORE-DESTROY fix: 5// let nh: *i64 = ss_open2(prefix, 1) 6// if nh[0] == 0 { ss_close(nh); sys_munmap(cur as *u8, DSC_SIGBUF); return dsc_handle } 7// Until this gate existed that fix was proven BY CONSTRUCTION only. MEASURED 2026-08-22 (pre-fix): one 8// transient empty manifest read destroyed the good handle, installed an empty one, memoised it against the 9// current signature, and every request served total=0 for ~8 minutes on a 1.8 GB / 17-segment index. 10// 11// HOW THE REAL CACHE PATH IS DRIVEN AGAINST A FIXTURE: the cache engages ONLY when the prefix is EXACTLY 12// "knowledge/store/dp-web-pub-" (dsc_web_prefix_is compares the whole literal; dss_prefix builds it from the 13// domain "web"), and that path is RELATIVE. So the gate sys_chdir's into /tmp/<gate>/ and builds 14// knowledge/store/ underneath it: the relative prefix then resolves into the fixture tree and every line of 15// the production open / refresh / guard code runs UNCHANGED -- no test hook, no prefix-override static, 16// no production path touched. The entry CWD (sys_getcwd) is restored before the verdict so gv_journal's 17// relative knowledge/status/harness.jrnl lands where every other gate's does. 18// FIXTURE-RATCHET LAW: all scratch under /tmp/nx_docportal_search_sticky_gate/, created by sys_mkdir at 19// SETUP (a teardown does not run when a run crashes); setup is IDEMPOTENT (the manifest is truncated before 20// the seed commit, so ss_commit -- max_segid+1 under the plane lock -- re-derives seg-1 on every run instead 21// of appending seg-N; stale segment FILES from an earlier run are overwritten or simply never named). 22// TEETH (each states itself; the verdict note does not recount them): 23// T0 accessor reports UNOPENED (-1) before any open (the -1 arm of dss_web_index_segments is real) 24// T1 fixture-reached-condition: healthy fixture opens with segments == 1 (a fixture that cannot fail is not a test) 25// T1b fixture-reached-condition: the cache memoised the HEALTHY manifest signature (size>0, == stat) 26// T2a fixture-reached-condition: the emptied manifest CHANGED the signature, so refresh reaches the reopen branch 27// T5 anti-vacuity: a direct ss_open2 of the emptied manifest returns ns==0 (T2 tests the condition, not a no-op) 28// T2 refresh-keeps-working-handle: refresh over the empty manifest -> still ready, SAME handle, still 1 segment 29// T3 signature-not-memoised: dsc_sig (read via dss_web_cache_sig) still equals the healthy signature, not the empty one 30// T3b WEAK-regression (passes on the pre-fix code too -- named so nobody reads it as discrimination) 31// T4 neg-control-reopen-on-real-change: a manifest naming REAL new segment bytes DOES replace the handle (1 -> 2) 32// T4b the replacement handle really serves the new bytes (ss_hget doc:2 == 1), so T4 counted segments, not names only 33// THIRD STATE: every setup step (mkdir / chdir / prefix / seed) is a gv_need; if one is missing the teeth are 34// NOT run and the gate ends SKIP, never RED -- a broken fixture must not read as a broken subject. 35// WHAT THIS GATE REQUIRES: the two accessors dss_web_index_segments / dss_web_cache_sig in 36// nx_docportal_search_seg.nx (round2 patch A1). Without them it does not compile -- by design: the gate 37// reads STATE, and the state is only readable through them (no importer in the estate reads another 38// module's static; measured over buildroot/runtime, every cross-file mention of a static is a comment). 39// license_tier: ORIGINAL Writes only under /tmp/nx_docportal_search_sticky_gate/. No hw writes (Rule 26). 40import "nx_docportal_search_seg.nx" 41import "nx_gate_verdict.nx" 42import "nx_syscalls.nx" 43 44// path scratch: the longest path built here is /tmp/<gate>/knowledge/store/dp-web-pub-manifest.txt (~70 B); 45// DSC_PREFIXBUF (512) is the seg file's own prefix scratch size and is reused for every path buffer so no 46// second size is invented. 47const STK_MANIFEST_SMALL: i64 = 16 // "seg-1" + LF is 6 bytes; 16 is one cache line, never a cap reached 48const STK_LF: i64 = 10 // the manifest row terminator ss_commit_body writes (nb[no] = 10) 49const STK_PUT: i64 = 1 // ss_add kind=1 = put (the kind every seeding gate uses) 50const STK_SEG1: i64 = 1 // first seeded segment id 51const STK_BOX: i64 = 16 // one out-box (ptr or len) -- a page-rounded mmap; the value is the box size, not a cap 52const STK_SEG2: i64 = 2 // second seeded segment id (the neg-control's REAL change) 53 54func stk_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 55func stk_path(out: *u8, a: *u8, b: *u8) -> i64 { 56 var o: i64 = ss_cat(out, 0, a) 57 o = ss_cat(out, o, b) 58 out[o] = 0 as u8 59 return o 60} 61// one seeded segment through the SAME writer path the crawler and every store uses (ss_begin/ss_add/ss_commit). 62// Returns ss_commit's rc (>= 0 committed). 63func stk_seed(prefix: *u8, segid: i64, key: *u8, text: *u8) -> i64 { 64 let w: *i64 = ss_begin() 65 ss_add(w, STK_PUT, key, text, stk_len(text)) 66 return ss_commit(prefix, w, segid) 67} 68// print " <label> a=<v1> b=<v2>\n" so every tooth carries its VALUES, not only PASS/FAIL 69func stk_show2(label: *u8, an: *u8, a: i64, bn: *u8, b: i64) -> i64 { 70 gv_puts(" " as *u8); gv_puts(label); gv_puts(" " as *u8) 71 gv_puts(an); gv_puts("=" as *u8); gv_num(a); gv_puts(" " as *u8) 72 gv_puts(bn); gv_puts("=" as *u8); gv_num(b); gv_puts("\n" as *u8) 73 return 0 74} 75func stk_exists(path: *u8) -> i64 { 76 let stb: *u8 = sys_mmap(DSC_STATBUF) 77 var ok: i64 = 0 78 if sys_fstatat(path, stb) == 0 { ok = 1 } 79 sys_munmap(stb, DSC_STATBUF) 80 return ok 81} 82 83func main(argc: i64, argv: *i64) -> i64 { 84 let ctr: *i64 = gv_ctr() 85 gv_head("NX-DOCPORTAL-SEARCH-STICKY-GATE -- an EMPTY reopen must keep the working web handle and must not memoise its signature" as *u8) 86 87 // ---- SETUP: /tmp/<gate>/knowledge/store/, then chdir so the PRODUCTION relative prefix resolves there ---- 88 let cwd0: *u8 = sys_mmap(SYS_PATH_MAX) 89 let cwdn: i64 = sys_getcwd(cwd0, SYS_PATH_MAX) 90 let root: *u8 = "/tmp/nx_docportal_search_sticky_gate" as *u8 91 let rk: *u8 = "/tmp/nx_docportal_search_sticky_gate/knowledge" as *u8 92 let rks: *u8 = "/tmp/nx_docportal_search_sticky_gate/knowledge/store" as *u8 93 sys_mkdir(root, MODE_0755) 94 sys_mkdir(rk, MODE_0755) 95 sys_mkdir(rks, MODE_0755) 96 let dirok: i64 = stk_exists(rks) 97 var chok: i64 = 0 98 if sys_chdir(root) == 0 { chok = 1 } 99 // the ONE prefix the cache engages on, built by the production builder from the production domain 100 let prefix: *u8 = sys_mmap(DSC_PREFIXBUF) 101 dss_prefix("web" as *u8, prefix) 102 let isweb: i64 = dsc_web_prefix_is(prefix) 103 let mpath: *u8 = sys_mmap(DSC_PREFIXBUF) 104 stk_path(mpath, prefix, "manifest.txt" as *u8) 105 // IDEMPOTENT SEED: truncate the manifest (O_TRUNC, n=0) so ss_max_segid reads "no segments" and ss_commit 106 // re-derives seg-1; then commit ONE real segment through the production writer. 107 ss_writefile(mpath, prefix, 0) 108 let seed1: i64 = stk_seed(prefix, STK_SEG1, "doc:1" as *u8, "stickyseed alpha document one" as *u8) 109 var seedok: i64 = 0 110 if seed1 >= 0 { if stk_exists(mpath) == 1 { seedok = 1 } } 111 112 gv_need("fixture root /tmp/nx_docportal_search_sticky_gate/knowledge/store exists (sys_mkdir at setup)" as *u8, dirok, ctr) 113 gv_need("chdir into the fixture root succeeded (the production RELATIVE prefix must resolve there)" as *u8, chok, ctr) 114 gv_need("prefix under test is the web prefix the cache engages on (dsc_web_prefix_is == 1)" as *u8, isweb, ctr) 115 gv_need("seed commit seg-1 succeeded through ss_commit (rc >= 0, manifest present)" as *u8, seedok, ctr) 116 var setup: i64 = 0 117 if dirok == 1 { if chok == 1 { if isweb == 1 { if seedok == 1 { setup = 1 } } } } 118 119 if setup == 1 { 120 // ---- T0: the accessor's UNOPENED arm is real (fresh process, nothing opened yet) ---- 121 let s0: i64 = dss_web_index_segments() 122 stk_show2("T0" as *u8, "segments_before_any_open" as *u8, s0, "expected" as *u8, 0 - 1) 123 var t0: i64 = 0 124 if s0 == (0 - 1) { t0 = 1 } 125 gv_check("T0 accessor reports UNOPENED (-1) before the first open" as *u8, t0, ctr) 126 127 // ---- T1: healthy fixture opens through the PARENT refresh path ---- 128 let r1: i64 = dss_web_cache_refresh() 129 let s1: i64 = dss_web_index_segments() 130 // request-path read of the cached handle pointer: with dsc_may_reopen == 0 and a handle present, 131 // dss_open_maybe_cached returns dsc_handle WITHOUT reopening (seg L187 / L192) 132 let p1: *i64 = dss_open_maybe_cached(prefix) 133 stk_show2("T1" as *u8, "refresh_rc" as *u8, r1, "segments" as *u8, s1) 134 var t1: i64 = 0 135 if r1 == 1 { if s1 == 1 { if (p1 as i64) != 0 { if p1[0] == s1 { t1 = 1 } } } } 136 gv_check("T1 fixture-reached-condition: healthy fixture opens with segments == 1 (accessor == handle h[0])" as *u8, t1, ctr) 137 let sig1: *i64 = sys_mmap(DSC_SIGBUF) as *i64 138 let hs1: i64 = dss_web_cache_sig(sig1) 139 let mf1: *i64 = sys_mmap(DSC_SIGBUF) as *i64 140 dsc_manifest_sig(prefix, mf1) 141 stk_show2("T1b" as *u8, "memoised_size" as *u8, sig1[0], "stat_size" as *u8, mf1[0]) 142 var t1b: i64 = 0 143 if hs1 == 1 { if sig1[0] > 0 { if sig1[0] == mf1[0] { if sig1[1] == mf1[1] { t1b = 1 } } } } 144 gv_check("T1b fixture-reached-condition: cache memoised the HEALTHY manifest signature (size>0, == stat)" as *u8, t1b, ctr) 145 146 // ---- THE CONDITION: the manifest reads EMPTY (0 bytes: ss_manifest_dyn sz<=0 -> 0 segments) ---- 147 ss_writefile(mpath, prefix, 0) 148 let mf2: *i64 = sys_mmap(DSC_SIGBUF) as *i64 149 dsc_manifest_sig(prefix, mf2) 150 stk_show2("T2a" as *u8, "empty_stat_size" as *u8, mf2[0], "healthy_stat_size" as *u8, mf1[0]) 151 var t2a: i64 = 0 152 if mf2[0] == 0 { if mf1[0] > 0 { t2a = 1 } } 153 gv_check("T2a fixture-reached-condition: emptied manifest CHANGED the signature (st_size >0 -> 0), so refresh reaches the reopen branch" as *u8, t2a, ctr) 154 // ---- T5 anti-vacuity: the production open REALLY sees ns==0 on this fixture ---- 155 let nh5: *i64 = ss_open2(prefix, 1) 156 let ns5: i64 = nh5[0] 157 ss_close(nh5) 158 stk_show2("T5" as *u8, "direct_ss_open2_ns" as *u8, ns5, "expected" as *u8, 0) 159 var t5: i64 = 0 160 if ns5 == 0 { t5 = 1 } 161 gv_check("T5 anti-vacuity: a direct ss_open2 of the emptied manifest returns ns==0 (T2 tests the real condition, not a no-op)" as *u8, t5, ctr) 162 163 // ---- T2: the parent refresh over the EMPTY manifest keeps the working handle ---- 164 let r2: i64 = dss_web_cache_refresh() 165 let s2: i64 = dss_web_index_segments() 166 let p2: *i64 = dss_open_maybe_cached(prefix) 167 stk_show2("T2" as *u8, "refresh_rc" as *u8, r2, "segments_after_empty_reopen" as *u8, s2) 168 var t2: i64 = 0 169 if r2 == 1 { if s2 == 1 { if (p2 as i64) == (p1 as i64) { t2 = 1 } } } 170 gv_check("T2 refresh-keeps-working-handle: after the EMPTY reopen, refresh is still ready and the SAME handle still holds 1 segment" as *u8, t2, ctr) 171 172 // ---- T3: the empty open did NOT pin dsc_sig (the next refresh will RETRY instead of waiting) ---- 173 let sig3: *i64 = sys_mmap(DSC_SIGBUF) as *i64 174 let hs3: i64 = dss_web_cache_sig(sig3) 175 stk_show2("T3" as *u8, "memoised_size_now" as *u8, sig3[0], "healthy_size" as *u8, sig1[0]) 176 var t3: i64 = 0 177 if hs3 == 1 { if sig3[0] == sig1[0] { if sig3[1] == sig1[1] { if sig3[0] != mf2[0] { t3 = 1 } } } } 178 gv_check("T3 signature-not-memoised: dsc_sig still equals the HEALTHY signature, not the empty manifest's" as *u8, t3, ctr) 179 180 // ---- T3b: restore the manifest and refresh -> a 1-segment handle (behavioural; also true pre-fix) ---- 181 let mb: *u8 = sys_mmap(STK_MANIFEST_SMALL) 182 var mo: i64 = ss_cat(mb, 0, "seg-1" as *u8) 183 mb[mo] = STK_LF as u8 184 mo = mo + 1 185 ss_writefile(mpath, mb, mo) 186 let r3: i64 = dss_web_cache_refresh() 187 let s3: i64 = dss_web_index_segments() 188 stk_show2("T3b" as *u8, "refresh_rc" as *u8, r3, "segments_after_restore" as *u8, s3) 189 var t3b: i64 = 0 190 if r3 == 1 { if s3 == 1 { t3b = 1 } } 191 gv_check("T3b WEAK-regression (passes on the pre-fix code too): manifest restored, refresh yields a 1-segment handle" as *u8, t3b, ctr) 192 193 // ---- T4 neg-control: a REAL change (new segment, different bytes) DOES replace the handle ---- 194 let seed2: i64 = stk_seed(prefix, STK_SEG2, "doc:2" as *u8, "stickyseed beta document two with different bytes" as *u8) 195 let mf4: *i64 = sys_mmap(DSC_SIGBUF) as *i64 196 dsc_manifest_sig(prefix, mf4) 197 let r4: i64 = dss_web_cache_refresh() 198 let s4: i64 = dss_web_index_segments() 199 let sig4: *i64 = sys_mmap(DSC_SIGBUF) as *i64 200 dss_web_cache_sig(sig4) 201 stk_show2("T4" as *u8, "segments_after_real_change" as *u8, s4, "memoised_size" as *u8, sig4[0]) 202 var t4: i64 = 0 203 if seed2 >= 0 { if r4 == 1 { if s4 == 2 { if sig4[0] == mf4[0] { if sig4[1] == mf4[1] { t4 = 1 } } } } } 204 gv_check("T4 neg-control-reopen-on-real-change: a manifest naming REAL new segment bytes replaces the handle (segments 1 -> 2) and memoises the NEW signature" as *u8, t4, ctr) 205 let p4: *i64 = dss_open_maybe_cached(prefix) 206 let dptr: *i64 = sys_mmap(STK_BOX) as *i64 207 let dlen: *i64 = sys_mmap(STK_BOX) as *i64 208 var got2: i64 = 0 209 if (p4 as i64) != 0 { if ss_hget(p4, "doc:2" as *u8, dptr, dlen) == 1 { got2 = 1 } } 210 stk_show2("T4b" as *u8, "hget_doc2" as *u8, got2, "handle_changed" as *u8, ((p4 as i64) != (p1 as i64)) as i64) 211 gv_check("T4b the replacement handle really serves the new bytes (ss_hget doc:2 == 1) -- T4 counted segments, not names only" as *u8, got2, ctr) 212 } 213 214 // restore the entry CWD so gv_journal appends to the estate's knowledge/status/harness.jrnl, not the fixture's 215 if cwdn > 0 { sys_chdir(cwd0) } 216 let rc: i64 = gv_verdict("DOCPORTAL-SEARCH-STICKY-GATE" as *u8, ctr, "an empty reopen keeps the working web handle and does not memoise its signature" as *u8) 217 sys_exit(rc) 218 return rc 219}