code wiki / _hdl_build / nx_web_shard_compact.nx

nx_web_shard_compact.nx source

↩ module page · 363 lines · 18606 B

1// nx_web_shard_compact.nx -- SCALE compaction for big multi-segment shards (the web shard's rung). 2// ss_compact/ss_compact_cap have EXACT semantics (last entry per key wins, tombstones kept, retired 3// segments archived then an atomic manifest swap) but their key dedup is a LINEAR table scan = 4// O(entries^2): fine at 4k keys, unusable at the web shard's ~1.4M entries (bulk docs + pr: priors). 5// This organ is the same merge with an open-addressing HASH dedup = O(entries), plus a post-swap 6// verification pass. Semantics kept byte-equal to ss_compact: chronological walk, last wins, ALL kinds 7// carried (incl. kind-2 tombstones), archive-before-swap, every failure path returns BEFORE the swap. 8// 9// BYTE-BUDGETED SUFFIX FOLD (2026-07-29): an unbounded whole-shard merge materializes the merged 10// segment in anonymous RAM (the writer buffer, plus aux blobs and key tables that all scale with 11// merged bytes) -- the OOM killer reaped it twice at bytes-in=1883885124 on 2026-07-25, mid-merge, 12// before the swap (never-brick held; shard untouched). The merge therefore now folds the LONGEST 13// NEWEST contiguous manifest suffix whose .docs bytes fit a budget derived from the box itself 14// (MemAvailable/WSC_BUDGET_SHARE; argv[2] in MB overrides). Replacing a contiguous suffix with its 15// fold preserves last-wins semantics EXACTLY: the run is merged in chronological order, kind-2 16// tombstones are carried, and older (kept) segments remain shadowed by the merged tail precisely as 17// they were by the unmerged tail. Repeated runs converge the shard LSM-style with bounded memory; 18// a streaming (file-backed) writer that lifts the budget entirely is the named next rung. 19// usage: nx_web_shard_compact <domain> [budget-MB] 20// license_tier: ORIGINAL 21import "nx_docportal_search_seg.nx" // dss_prefix + nx_seg_store (ss_* primitives) 22const WSC_MAGIC_1125899906842597: i64 = 1125899906842597 23const WSC_MAGIC_1048576: i64 = 1048576 24const WSC_MAGIC_65536: i64 = 65536 25 26const WSC_MAXSEGS: i64 = 8192 // manifest lines we can fold in one run 27const WSC_MAXENT: i64 = 134217728 // hard sanity ceiling on entries (2^27; ~an order past any near plan) 28const WSC_HMUL: i64 = 7046029254386353131 // odd 63-bit multiplicative-mix constant (same family as pagerank_build) 29const WSC_BUDGET_SHARE: i64 = 32 // /32 (was /16) 2026-07-30: the /16 reasoning below is RIGHT about the 9-10x amplification but it reserves a share of MemAvailable AT START on a box with OTHER GROWING CONSUMERS (torrent stack, jellyfin, syno services) -- the share is stale the moment it is computed, and it assumes this job is the only claimant. MEASURED TODAY: with ~25GB available at start the /16 budget projected a peak of ~15GB and this organ was caught at 21GB RSS with 536MB free, minutes from OOM, taking mgmt + the tools daemon down with it. /32 projects ~7-8GB, which also keeps it clear of the 16GiB RLIMIT_AS ceiling now imposed by hc_spawn_searchpipe_job -- TWO CONTROLS THAT CONTRADICT EACH OTHER ARE ONE CONTROL AND ONE OUTAGE, so the soft budget must project a peak BELOW the hard cap. Original note, still accurate on the amplification: fold budget = MemAvailable/16. MEASURED 2026-07-29 (death #3, OOM in ss_build_terms): a 1.88GB run took MemAvailable from 19.4GB to 1.2GB before the kill => PEAK TOUCH ~9-10x run bytes (occ arrays + pairs + writer + key tables + input page cache), not the ~6x the seq1048 memo estimated. /16 caps projected peak at ~60% of MemAvailable so the box keeps serving 30const WSC_BUDGET_FALLBACK: i64 = 268435456 // 256MB: used ONLY when /proc/meminfo is unreadable -- conservative enough to be safe on any box that boots this stack, keeps the pinned no-arg hostctl sub from wedging into a permanent refuse 31const WSC_BUDGET_FLOOR: i64 = 67108864 // 64MB: below this the box is too tight to merge anything safely -> refuse loudly rather than thrash 32const WSC_LOCK_EX: i64 = 2 // flock LOCK_EX on <prefix>plock -- the plane-lock discipline segguard + nx_seg_compact_cli already honor; a lockless manifest swap under a concurrent fold LOSES rows (the proven seq349 race) 33const WSC_MEMINFO_BUF: i64 = 8192 // scratch size for the bounded /proc/meminfo read (file is ~1.5KB) 34const WSC_MEMINFO_CAP: i64 = 7900 // read cap under the scratch -- headroom below WSC_MEMINFO_BUF 35const WSC_KB: i64 = 1024 // /proc/meminfo reports MemAvailable in kB 36 37func wc9_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 38func wc9_num(v: i64) -> i64 { 39 let bb: *u8 = sys_mmap(28); var m: i64 = v 40 if m < 0 { sys_write(1, "-" as *u8, 1); m = 0 - m } 41 let t: *u8 = sys_mmap(28); var k: i64 = 0 42 if m == 0 { t[0] = 48 as u8; k = 1 } 43 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 44 var i: i64 = 0; while i < k { bb[i] = t[k - 1 - i]; i = i + 1 } 45 sys_write(1, bb, k); return 0 46} 47// polynomial hash of key bytes -> mixed slot (same ci_hash polynomial so behavior is well-understood). 48// hshift/hmask parameterized: the table is sized DATA-DRIVEN from a first-pass entry count. 49func wsc_slot(k: *u8, kl: i64, hshift: i64, hmask: i64) -> i64 { 50 var h: i64 = WSC_MAGIC_1125899906842597 51 var i: i64 = 0 52 while i < kl { h = (h * 131) + (k[i] as i64); i = i + 1 } 53 var s: i64 = h * WSC_HMUL 54 s = s & 0x7fffffffffffffff 55 return (s >> hshift) & hmask 56} 57// MemAvailable in BYTES from /proc/meminfo -- the kernel's own "allocatable without swapping" figure. 58// procfs files report size 0 to lseek, so ss_readall (lseek-sized buffer) CANNOT read them (its 64KB 59// reads would overflow a 64-byte buffer); this is a bounded read loop into a fixed 8KB scratch instead. 60// Returns -1 if unreadable or unparseable -- callers must treat that as "cannot judge", never as 0. 61func wsc_memavail() -> i64 { 62 let fd: i64 = sys_openat_rd("/proc/meminfo" as *u8) 63 if fd < 0 { return 0 - 1 } 64 let buf: *u8 = sys_mmap(WSC_MEMINFO_BUF) 65 var got: i64 = 0 66 var n: i64 = 1 67 while n > 0 { 68 if got >= WSC_MEMINFO_CAP { n = 0 } else { 69 n = sys_read(fd, (buf as i64 + got) as *u8, WSC_MEMINFO_CAP - got) 70 if n > 0 { got = got + n } 71 } 72 } 73 sys_close(fd) 74 if got <= 0 { return 0 - 1 } 75 let pat: *u8 = "MemAvailable:" as *u8 76 var i: i64 = 0 77 while i + 13 < got { 78 var hit: i64 = 1 79 var j: i64 = 0 80 while j < 13 { if buf[i + j] != pat[j] { hit = 0; j = 13 } else { j = j + 1 } } 81 if hit == 1 { 82 var v: i64 = 0 83 var seen: i64 = 0 84 var k2: i64 = i + 13 85 while k2 < got { 86 let c: i64 = buf[k2] as i64 87 if c == 10 { k2 = got } else { 88 if c >= 48 { if c <= 57 { v = v * 10 + (c - 48); seen = 1 } } 89 k2 = k2 + 1 90 } 91 } 92 if seen == 1 { return v * WSC_KB } 93 return 0 - 1 94 } 95 i = i + 1 96 } 97 return 0 - 1 98} 99 100func main(argc: i64, argv: *i64) -> i64 { 101 var domain: *u8 = "web" as *u8 102 if argc >= 2 { domain = argv[1] as *u8 } 103 let prefix: *u8 = sys_mmap(512) 104 dss_prefix(domain, prefix) 105 // PLANE LOCK FIRST: serialize vs segguard + nx_seg_compact_cli (both flock <prefix>plock). 106 // Blocking LOCK_EX -- a short wait behind a sibling fold is correct; running beside one is not. 107 let lkp: *u8 = sys_mmap(512) 108 var lo: i64 = 0 109 lo = ss_cat(lkp, lo, prefix) 110 lo = ss_cat(lkp, lo, "plock" as *u8) 111 lkp[lo] = 0 as u8 112 let lfd: i64 = sys_openat_append(lkp, 0x1a4) 113 if lfd < 0 { wc9_puts("cannot open plane lock\nverdict=FAIL\n" as *u8); return 13 } 114 if sys_flock(lfd, WSC_LOCK_EX) != 0 { wc9_puts("cannot take plane lock\nverdict=FAIL\n" as *u8); return 13 } 115 let segs: *i64 = sys_mmap(8 * WSC_MAXSEGS) as *i64 116 let ns: i64 = ss_manifest_cap(prefix, segs, WSC_MAXSEGS) 117 wc9_puts("=== nx_web_shard_compact " as *u8); wc9_puts(prefix); wc9_puts(" segments=" as *u8); wc9_num(ns); wc9_puts(" ===\n" as *u8) 118 if ns <= 0 { wc9_puts("no live segments\nverdict=FAIL\n" as *u8); return 1 } 119 if ns == 1 { wc9_puts("already a single segment -- nothing to fold\nverdict=PASS\n" as *u8); return 0 } 120 121 // fold-run byte budget: argv[2] MB if given, else MemAvailable/WSC_BUDGET_SHARE, else fallback. 122 var budget: i64 = 0 123 if argc >= 3 { 124 let a2c: *u8 = argv[2] as *u8 125 var bi: i64 = 0 126 while a2c[bi] != (0 as u8) { let c: i64 = a2c[bi] as i64; if c >= 48 { if c <= 57 { budget = budget * 10 + (c - 48) } } bi = bi + 1 } 127 budget = budget * WSC_MAGIC_1048576 128 } 129 if budget <= 0 { 130 let ma: i64 = wsc_memavail() 131 if ma > 0 { budget = ma / WSC_BUDGET_SHARE } 132 if ma <= 0 { budget = WSC_BUDGET_FALLBACK; wc9_puts("MemAvailable unreadable -> conservative fallback budget\n" as *u8) } 133 } 134 wc9_puts("fold-budget bytes=" as *u8); wc9_num(budget); wc9_puts("\n" as *u8) 135 if budget < WSC_BUDGET_FLOOR { wc9_puts("budget below floor -- box too tight to merge safely, refusing\nverdict=FAIL\n" as *u8); return 11 } 136 137 // fresh merged segid = manifest max + 1 (numeric part of "seg-<id>") 138 var segid: i64 = 1 139 var si: i64 = 0 140 while si < ns { 141 let nm: *u8 = segs[si] as *u8 142 var v: i64 = 0; var ci: i64 = 0 143 while nm[ci] != (0 as u8) { let c: i64 = nm[ci] as i64; if c >= 48 { if c <= 57 { v = v*10 + (c-48) } } ci = ci + 1 } 144 if v >= segid { segid = v + 1 } 145 si = si + 1 146 } 147 148 // read every live segment (chronological = manifest order) 149 let bptrs: *i64 = sys_mmap(8 * WSC_MAXSEGS) as *i64 150 let bszs: *i64 = sys_mmap(8 * WSC_MAXSEGS) as *i64 151 var total: i64 = 0 152 var s: i64 = 0 153 while s < ns { 154 let path: *u8 = sys_mmap(512) 155 var o: i64 = 0 156 o = ss_cat(path, o, prefix) 157 o = ss_cat(path, o, segs[s] as *u8) 158 o = ss_cat(path, o, ".docs" as *u8) 159 path[o] = 0 as u8 160 let szp: *i64 = sys_mmap(16) as *i64 161 bptrs[s] = ss_loadfile(path, szp, 1) as i64 // mmap-open the input segments: sequential read, low RSS 162 bszs[s] = szp[0] 163 if bszs[s] < 0 { bszs[s] = 0 } 164 total = total + bszs[s] 165 s = s + 1 166 } 167 168 // SUFFIX-RUN SELECTION: longest newest contiguous run with run-bytes <= budget. kk = first 169 // manifest index INSIDE the run; [0..kk-1] are kept verbatim, [kk..ns-1] fold into one segment. 170 var kk: i64 = ns - 1 171 var runbytes: i64 = bszs[ns - 1] 172 var walking: i64 = 1 173 while walking == 1 { 174 if kk == 0 { walking = 0 } else { 175 if runbytes + bszs[kk - 1] <= budget { runbytes = runbytes + bszs[kk - 1]; kk = kk - 1 } else { walking = 0 } 176 } 177 } 178 if ns - kk < 2 { 179 wc9_puts("newest two segments exceed the fold budget -- nothing foldable at this budget (streaming writer is the next rung)\nverdict=FAIL\n" as *u8) 180 return 12 181 } 182 wc9_puts("fold-run: keep=" as *u8); wc9_num(kk); wc9_puts(" fold=" as *u8); wc9_num(ns - kk) 183 wc9_puts(" run-bytes=" as *u8); wc9_num(runbytes); wc9_puts(" total-bytes=" as *u8); wc9_num(total); wc9_puts("\n" as *u8) 184 185 // PASS 1 over the RUN ONLY: count real entries (record framing walk, no fixed guess) 186 var nent: i64 = 0 187 var s0: i64 = kk 188 while s0 < ns { 189 let b0: *u8 = bptrs[s0] as *u8 190 let sz0: i64 = bszs[s0] 191 var i0: i64 = 0 192 while i0 + 9 <= sz0 { 193 let kl0: i64 = ss_r32(b0, i0 + 1) 194 let vl0: i64 = ss_r32(b0, i0 + 5 + kl0) 195 i0 = i0 + 5 + kl0 + 4 + vl0 196 nent = nent + 1 197 } 198 s0 = s0 + 1 199 } 200 if nent > WSC_MAXENT { wc9_puts("entry count exceeds WSC_MAXENT sanity ceiling\nverdict=FAIL\n" as *u8); return 2 } 201 let maxk: i64 = nent + 16 202 // hash table = smallest power of two >= 4x entries (load factor <= 0.25) 203 var hsize: i64 = WSC_MAGIC_1048576 204 var hbits: i64 = 20 205 while hsize < nent * 4 { hsize = hsize * 2; hbits = hbits + 1 } 206 let hshift: i64 = 63 - hbits 207 let hmask: i64 = hsize - 1 208 wc9_puts("pass1 entries=" as *u8); wc9_num(nent); wc9_puts(" hash-slots=" as *u8); wc9_num(hsize); wc9_puts("\n" as *u8) 209 210 // entry tables (dense) + hash slot -> entry-index (+1 so 0 = empty) 211 let tkp: *i64 = sys_mmap(8 * maxk) as *i64 212 let tkl: *i64 = sys_mmap(8 * maxk) as *i64 213 let tkind: *i64 = sys_mmap(8 * maxk) as *i64 214 let tvp: *i64 = sys_mmap(8 * maxk) as *i64 215 let tvl: *i64 = sys_mmap(8 * maxk) as *i64 216 let hslot: *i64 = sys_mmap(8 * hsize) as *i64 217 var nk: i64 = 0 218 var entries: i64 = 0 219 s = kk 220 while s < ns { 221 let b: *u8 = bptrs[s] as *u8 222 let sz: i64 = bszs[s] 223 var i: i64 = 0 224 while i + 9 <= sz { 225 let kind: i64 = b[i] 226 let kl: i64 = ss_r32(b, i + 1) 227 let koff: i64 = i + 5 228 let vl: i64 = ss_r32(b, koff + kl) 229 let voff: i64 = koff + kl + 4 230 let kp: *u8 = (b as i64 + koff) as *u8 231 // hash-probe for this key (byte-verify on hit; O(1) expected) 232 var slot: i64 = wsc_slot(kp, kl, hshift, hmask) 233 var hit: i64 = 0 - 1 234 var probing: i64 = 1 235 while probing == 1 { 236 let e: i64 = hslot[slot] 237 if e == 0 { probing = 0 } else { 238 if ss_kcmp(tkp[e - 1] as *u8, tkl[e - 1], kp, kl) == 0 { hit = e - 1; probing = 0 } else { 239 slot = (slot + 1) & hmask 240 } 241 } 242 } 243 if hit < 0 { 244 if nk >= maxk { wc9_puts("key table overflow (impossible: data-driven bound)\nverdict=FAIL\n" as *u8); return 3 } 245 hit = nk 246 hslot[slot] = nk + 1 247 nk = nk + 1 248 } 249 tkp[hit] = kp as i64 250 tkl[hit] = kl 251 tkind[hit] = kind 252 tvp[hit] = (b as i64) + voff 253 tvl[hit] = vl 254 entries = entries + 1 255 i = voff + vl 256 } 257 s = s + 1 258 } 259 wc9_puts("entries=" as *u8); wc9_num(entries); wc9_puts(" live-keys=" as *u8); wc9_num(nk) 260 wc9_puts(" run-bytes-in=" as *u8); wc9_num(runbytes); wc9_puts("\n" as *u8) 261 262 // merged segment (latest per key, insertion order = first-seen key order, matching ss_compact) 263 let w: *i64 = ss_begin_cap(runbytes + WSC_MAGIC_65536) 264 var t2: i64 = 0 265 while t2 < nk { 266 if ss_add2(w, tkind[t2], tkp[t2] as *u8, tkl[t2], tvp[t2] as *u8, tvl[t2]) != 0 { wc9_puts("writer overflow\nverdict=FAIL\n" as *u8); return 4 } 267 t2 = t2 + 1 268 } 269 wc9_puts("merge-write: emitting seg (docs+idx+pos+imp)...\n" as *u8) 270 if ss_write_seg(prefix, w, segid) != 0 { wc9_puts("segment write failed\nverdict=FAIL\n" as *u8); return 5 } 271 wc9_puts("merged segment written\n" as *u8) 272 273 // archive retired names BEFORE the swap (crash-safe ordering, identical to ss_compact) 274 let ap: *u8 = sys_mmap(512) 275 var ao: i64 = 0 276 ao = ss_cat(ap, ao, prefix) 277 ao = ss_cat(ap, ao, "manifest-archive.txt" as *u8) 278 ap[ao] = 0 as u8 279 let afd: i64 = sys_openat_append(ap, 0x1a4) 280 if afd < 0 { wc9_puts("archive open failed\nverdict=FAIL\n" as *u8); return 6 } 281 s = kk 282 while s < ns { 283 let nm2: *u8 = segs[s] as *u8 284 sys_write(afd, nm2, ss_len(nm2)) 285 sys_write(afd, "\n" as *u8, 1) 286 s = s + 1 287 } 288 sys_fsync(afd) 289 sys_close(afd) 290 // atomic manifest swap -> kept prefix lines [0..kk-1] + the merged segment (order preserved) 291 let mf: *u8 = sys_mmap(512) 292 let mt: *u8 = sys_mmap(512) 293 var o2: i64 = 0 294 o2 = ss_cat(mf, o2, prefix) 295 o2 = ss_cat(mf, o2, "manifest.txt" as *u8) 296 mf[o2] = 0 as u8 297 o2 = 0 298 o2 = ss_cat(mt, o2, prefix) 299 o2 = ss_cat(mt, o2, "manifest.tmp" as *u8) 300 mt[o2] = 0 as u8 301 let nb: *u8 = sys_mmap(40 * ns + 256) 302 var no: i64 = 0 303 var km: i64 = 0 304 while km < kk { 305 no = ss_cat(nb, no, segs[km] as *u8) 306 nb[no] = 10 as u8 307 no = no + 1 308 km = km + 1 309 } 310 no = ss_cat(nb, no, "seg-" as *u8) 311 no = ss_catn(nb, no, segid) 312 nb[no] = 10 as u8 313 no = no + 1 314 if ss_writefile(mt, nb, no) != 0 { wc9_puts("manifest tmp write failed\nverdict=FAIL\n" as *u8); return 7 } 315 if sys_renameat(mt, mf) != 0 { wc9_puts("manifest swap failed\nverdict=FAIL\n" as *u8); return 8 } 316 ss_syncdir(prefix) 317 318 // POST-SWAP VERIFY: reopen; every live key must resolve via ss_hget to the SAME bytes we merged 319 // (sampled stride keeps this O(nk/stride); stride 97 ~= 1% coverage floor + first/last always). 320 // ss_open2(...,1) = mmap open: the read-all ss_open pulls the WHOLE shard into anon RAM -- the 321 // same OOM class this rewrite exists to kill; verify must not die of the disease it cures. 322 let h2: *i64 = ss_open2(prefix, 1) 323 if (h2 as i64) == 0 { wc9_puts("VERIFY reopen failed\nverdict=FAIL\n" as *u8); return 9 } 324 let vp: *i64 = sys_mmap(16) as *i64 325 let vl2: *i64 = sys_mmap(16) as *i64 326 let kbuf: *u8 = sys_mmap(600) 327 var bad: i64 = 0 328 var checked: i64 = 0 329 var t3: i64 = 0 330 while t3 < nk { 331 var pick: i64 = 0 332 if t3 == 0 { pick = 1 } 333 if t3 == nk - 1 { pick = 1 } 334 if (t3 % 97) == 0 { pick = 1 } 335 if pick == 1 { if tkind[t3] == 1 { if tkl[t3] < 590 { 336 let sp2: *u8 = tkp[t3] as *u8 337 var x: i64 = 0 338 while x < tkl[t3] { kbuf[x] = sp2[x]; x = x + 1 } 339 kbuf[tkl[t3]] = 0 as u8 340 checked = checked + 1 341 if ss_hget(h2, kbuf, vp, vl2) != 1 { bad = bad + 1 } else { 342 if vl2[0] != tvl[t3] { bad = bad + 1 } else { 343 let a2: *u8 = vp[0] as *u8 344 let b2: *u8 = tvp[t3] as *u8 345 var y: i64 = 0 346 var mism: i64 = 0 347 while y < vl2[0] { if a2[y] != b2[y] { mism = 1; y = vl2[0] } else { y = y + 1 } } 348 if mism == 1 { bad = bad + 1 } 349 } 350 } 351 } } } 352 t3 = t3 + 1 353 } 354 wc9_puts("COMPACT done: segments " as *u8); wc9_num(ns); wc9_puts(" -> " as *u8); wc9_num(kk + 1); wc9_puts(" (folded tail -> seg-" as *u8); wc9_num(segid) 355 wc9_puts(") live-keys=" as *u8); wc9_num(nk) 356 wc9_puts(" verify-checked=" as *u8); wc9_num(checked); wc9_puts(" verify-bad=" as *u8); wc9_num(bad); wc9_puts("\n" as *u8) 357 if bad > 0 { 358 wc9_puts("VERIFY FAILED -- restore <prefix>manifest.txt from the retired list in manifest-archive.txt\nverdict=FAIL\n" as *u8) 359 return 10 360 } 361 wc9_puts("verdict=PASS\n" as *u8) 362 return 0 363}