code wiki / _hdl_build / nx_web_shard_compact.nx

nx_web_shard_compact.nx source

↩ module page · 464 lines · 27291 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 folds a contiguous run 13// whose .docs bytes fit a budget derived from the box itself (MemAvailable/WSC_BUDGET_SHARE, clamped 14// to WSC_BUDGET_CEIL; argv[2] in MB overrides, same clamp). Repeated runs converge the shard 15// LSM-style with bounded memory; a streaming (file-backed) writer that lifts the budget entirely is 16// the named next rung. 17// 18// RANGE FOLD (2026-08-10, supersedes suffix-only selection). The suffix rule stalls forever at the 19// first FAT segment: once a merged product nears the budget it heads every suffix walk (runbytes 20// starts at the tail), so each later fold must carry it and the hundreds of small segments BEHIND it 21// can never be reached -- measured on this shard: ~836 pre-08-08 segments stranded behind a ~240MB 22// merged tail while the live count climbed 878 -> 1205 and query latency followed. A fold of ANY 23// contiguous run, replaced IN PLACE in the manifest, preserves last-wins semantics by the same 24// argument the suffix fold's header makes: shadowing depends only on RELATIVE manifest order, and the 25// merged segment sits exactly where the run sat -- newer kept segments still shadow it, and it still 26// shadows older ones, precisely as the unmerged run did. Kind-2 tombstones are carried as before. 27// FAT SKIP: a segment >= budget/WSC_FAT_DIV is an already-merged product; re-folding it buys almost 28// no count reduction per byte, so runs are chosen from non-fat segments only, newest first. 29// usage: nx_web_shard_compact <domain> [budget-MB] 30// license_tier: ORIGINAL 31import "nx_docportal_search_seg.nx" // dss_prefix + nx_seg_store (ss_* primitives) 32import "nx_heavyio_lib.nx" // estate-wide heavy-I/O bound (2026-09-03): a scale fold is pure seg-store maintenance -- it yields to the storm BEFORE taking the plane lock 33const WSC_MAGIC_1125899906842597: i64 = 1125899906842597 34const WSC_MAGIC_1048576: i64 = 1048576 35const WSC_MAGIC_65536: i64 = 65536 36 37const WSC_MAXSEGS: i64 = 8192 // manifest lines we can fold in one run 38const WSC_MAXENT: i64 = 134217728 // hard sanity ceiling on entries (2^27; ~an order past any near plan) 39const WSC_HMUL: i64 = 7046029254386353131 // odd 63-bit multiplicative-mix constant (same family as pagerank_build) 40const 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 41const WSC_BUDGET_CEIL: i64 = 209715200 // 200MB HARD CEILING on the fold budget, argv and auto alike. MEASURED: explicit budgets 128/200/230MB all PASSed (2026-08-08) while 256MB died ENOMEM in ss_build_terms under the 16GiB RLIMIT_AS; on 2026-08-10 the UNCLAMPED auto budget (MemAvailable/32 = ~744MB with ~23GB free) died in merge-write on EVERY spawner run -- the searchcompact spawner passes no budget, so the unattended path MUST be self-capping or compaction is structurally dead exactly when nobody is watching. /32 bounds the SHARE of the box; this bounds the ABSOLUTE peak against the address-space cap. Raise only with a re-measured amplification factor. 42const WSC_FAT_DIV: i64 = 2 // fat threshold divisor: a segment >= budget/WSC_FAT_DIV is an already-merged product -- skipped by run selection, never re-folded at this budget 43const 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 (the CEIL clamp above still applies) 44const WSC_BUDGET_FLOOR: i64 = 67108864 // 64MB: below this the box is too tight to merge anything safely -> refuse loudly rather than thrash 45const 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) 46const WSC_MEMINFO_BUF: i64 = 8192 // scratch size for the bounded /proc/meminfo read (file is ~1.5KB) 47const WSC_MEMINFO_CAP: i64 = 7900 // read cap under the scratch -- headroom below WSC_MEMINFO_BUF 48const WSC_KB: i64 = 1024 // /proc/meminfo reports MemAvailable in kB 49 50func 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 } 51func wc9_num(v: i64) -> i64 { 52 let bb: *u8 = sys_mmap(28); var m: i64 = v 53 if m < 0 { sys_write(1, "-" as *u8, 1); m = 0 - m } 54 let t: *u8 = sys_mmap(28); var k: i64 = 0 55 if m == 0 { t[0] = 48 as u8; k = 1 } 56 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 57 var i: i64 = 0; while i < k { bb[i] = t[k - 1 - i]; i = i + 1 } 58 sys_write(1, bb, k); return 0 59} 60// polynomial hash of key bytes -> mixed slot (same ci_hash polynomial so behavior is well-understood). 61// hshift/hmask parameterized: the table is sized DATA-DRIVEN from a first-pass entry count. 62func wsc_slot(k: *u8, kl: i64, hshift: i64, hmask: i64) -> i64 { 63 var h: i64 = WSC_MAGIC_1125899906842597 64 var i: i64 = 0 65 while i < kl { h = (h * 131) + (k[i] as i64); i = i + 1 } 66 var s: i64 = h * WSC_HMUL 67 s = s & 0x7fffffffffffffff 68 return (s >> hshift) & hmask 69} 70// MemAvailable in BYTES from /proc/meminfo -- the kernel's own "allocatable without swapping" figure. 71// procfs files report size 0 to lseek, so ss_readall (lseek-sized buffer) CANNOT read them (its 64KB 72// reads would overflow a 64-byte buffer); this is a bounded read loop into a fixed 8KB scratch instead. 73// Returns -1 if unreadable or unparseable -- callers must treat that as "cannot judge", never as 0. 74func wsc_memavail() -> i64 { 75 let fd: i64 = sys_openat_rd("/proc/meminfo" as *u8) 76 if fd < 0 { return 0 - 1 } 77 let buf: *u8 = sys_mmap(WSC_MEMINFO_BUF) 78 var got: i64 = 0 79 var n: i64 = 1 80 while n > 0 { 81 if got >= WSC_MEMINFO_CAP { n = 0 } else { 82 n = sys_read(fd, (buf as i64 + got) as *u8, WSC_MEMINFO_CAP - got) 83 if n > 0 { got = got + n } 84 } 85 } 86 sys_close(fd) 87 if got <= 0 { return 0 - 1 } 88 let pat: *u8 = "MemAvailable:" as *u8 89 var i: i64 = 0 90 while i + 13 < got { 91 var hit: i64 = 1 92 var j: i64 = 0 93 while j < 13 { if buf[i + j] != pat[j] { hit = 0; j = 13 } else { j = j + 1 } } 94 if hit == 1 { 95 var v: i64 = 0 96 var seen: i64 = 0 97 var k2: i64 = i + 13 98 while k2 < got { 99 let c: i64 = buf[k2] as i64 100 if c == 10 { k2 = got } else { 101 if c >= 48 { if c <= 57 { v = v * 10 + (c - 48); seen = 1 } } 102 k2 = k2 + 1 103 } 104 } 105 if seen == 1 { return v * WSC_KB } 106 return 0 - 1 107 } 108 i = i + 1 109 } 110 return 0 - 1 111} 112 113func main(argc: i64, argv: *i64) -> i64 { 114 var domain: *u8 = "web" as *u8 115 if argc >= 2 { domain = argv[1] as *u8 } 116 let prefix: *u8 = sys_mmap(512) 117 dss_prefix(domain, prefix) 118 // HEAVY-I/O ADMISSION BEFORE THE PLANE LOCK (2026-09-03): a scale fold is pure seg-store maintenance, so under 119 // a storm it YIELDS (exit 4, verdict=DEFER -- a third state, never FAIL) and must never sit holding <prefix>plock 120 // while it waits -- the shape nx_seg_compact_cli adopted. HIO_UNOBSERVABLE (conf unreadable) PROCEEDS: maintenance 121 // is never blocked forever by a missing conf, and the announce line names the state that was measured. 122 let hio: *i64 = (sys_mmap(HIO_C_BYTES)) as *i64 123 let hv: i64 = hio_admit(hio) 124 hio_announce(1, hv, hio) 125 if hv == HIO_DEFER { wc9_puts("nx_web_shard_compact DEFERRED: heavy-I/O bound reached; requeue and re-fire\nverdict=DEFER\n" as *u8); return 4 } 126 // PLANE LOCK FIRST: serialize vs segguard + nx_seg_compact_cli (both flock <prefix>plock). 127 // Blocking LOCK_EX -- a short wait behind a sibling fold is correct; running beside one is not. 128 let lkp: *u8 = sys_mmap(512) 129 var lo: i64 = 0 130 lo = ss_cat(lkp, lo, prefix) 131 lo = ss_cat(lkp, lo, "plock" as *u8) 132 lkp[lo] = 0 as u8 133 let lfd: i64 = sys_openat_append(lkp, 0x1a4) 134 if lfd < 0 { wc9_puts("cannot open plane lock\nverdict=FAIL\n" as *u8); return 13 } 135 if sys_flock(lfd, WSC_LOCK_EX) != 0 { wc9_puts("cannot take plane lock\nverdict=FAIL\n" as *u8); return 13 } 136 let segs: *i64 = sys_mmap(8 * WSC_MAXSEGS) as *i64 137 let ns: i64 = ss_manifest_cap(prefix, segs, WSC_MAXSEGS) 138 wc9_puts("=== nx_web_shard_compact " as *u8); wc9_puts(prefix); wc9_puts(" segments=" as *u8); wc9_num(ns); wc9_puts(" ===\n" as *u8) 139 if ns <= 0 { wc9_puts("no live segments\nverdict=FAIL\n" as *u8); return 1 } 140 if ns == 1 { wc9_puts("already a single segment -- nothing to fold\nverdict=PASS\n" as *u8); return 0 } 141 142 // fold-run byte budget: argv[2] MB if given, else MemAvailable/WSC_BUDGET_SHARE, else fallback. 143 var budget: i64 = 0 144 if argc >= 3 { 145 let a2c: *u8 = argv[2] as *u8 146 var bi: i64 = 0 147 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 } 148 budget = budget * WSC_MAGIC_1048576 149 } 150 if budget <= 0 { 151 let ma: i64 = wsc_memavail() 152 if ma > 0 { budget = ma / WSC_BUDGET_SHARE } 153 if ma <= 0 { budget = WSC_BUDGET_FALLBACK; wc9_puts("MemAvailable unreadable -> conservative fallback budget\n" as *u8) } 154 } 155 // HARD CEILING, argv and auto alike (see WSC_BUDGET_CEIL). Printed so a clamped run is never 156 // mistaken for a run at the requested budget -- a silent cap becomes a measurement nobody knows 157 // is partial. 158 if budget > WSC_BUDGET_CEIL { 159 wc9_puts("budget clamped " as *u8); wc9_num(budget); wc9_puts(" -> " as *u8); wc9_num(WSC_BUDGET_CEIL) 160 wc9_puts(" (ss_build_terms peak ~9-10x run bytes vs 16GiB RLIMIT_AS; 256MB measured ENOMEM 2026-08-08, unclamped auto ~744MB died every run 2026-08-10)\n" as *u8) 161 budget = WSC_BUDGET_CEIL 162 } 163 wc9_puts("fold-budget bytes=" as *u8); wc9_num(budget); wc9_puts("\n" as *u8) 164 if budget < WSC_BUDGET_FLOOR { wc9_puts("budget below floor -- box too tight to merge safely, refusing\nverdict=FAIL\n" as *u8); return 11 } 165 166 // fresh merged segid -- DELEGATED to ss_next_segid (the seq1730 REPAIR half), not rolled locally. 167 // The local max+1 scan this replaces took the max over EVERY id in the manifest, INCLUDING poisoned 168 // pointer-band ones. This shard carries seg-140712850411539 (= 0x7FFF...), written by some earlier 169 // caller that stored a pointer where a parsed id belonged, so max+1 inherited the poison and 170 // ss_write_seg's guard correctly REFUSED the write -- MEASURED 2026-08-08: fold reached 171 // "merge-write: emitting seg" then failed with "segid=140712850411540 is an ADDRESS", verdict=FAIL, 172 // NOTHING WRITTEN. seq1730 shipped in two halves and this organ had adopted only the refusing one, 173 // so the guard could fire forever and compaction could never succeed. 174 // ss_max_segid (which ss_next_segid delegates to) SKIPS the pointer band by design -- "a corrupted 175 // manifest heals itself on the next write instead of staying pinned near 1.4e14 forever" -- so this 176 // both unblocks the fold and repairs the id sequence. It returns max-SANE + 1, which is greater than 177 // every sane id present and distinct from every poisoned one, so it cannot collide. 178 // ONE max-segid rule in the tree (rule 15): two copies of it is exactly how the two drift apart. 179 let segid: i64 = ss_next_segid(prefix) 180 if ss_segid_ok(segid) == 0 { wc9_puts("ss_next_segid returned an unusable id -- refusing before any write\nverdict=FAIL\n" as *u8); return 13 } 181 182 // read every live segment (chronological = manifest order) 183 let bptrs: *i64 = sys_mmap(8 * WSC_MAXSEGS) as *i64 184 let bszs: *i64 = sys_mmap(8 * WSC_MAXSEGS) as *i64 185 // HOISTED OUT OF THE LOOP (2026-08-06). These were allocated PER SEGMENT and never freed, so a 186 // manifest with N live segments burned N pages for `path` plus N for `szp` -- sys_mmap here is 187 // page-granular, so a 512-byte path costs 4096 bytes and its own kernel VMA. 188 // THIS IS NOT THEORETICAL: dmesg records this organ dying THREE TIMES, roughly hourly, at 189 // `segfault at fffffffffffffff4`. That address is -12 = ENOMEM. mmap failed, NOTHING CHECKED THE 190 // RETURN, and the next write went straight through the error code -- the exact endgame written 191 // down in sys_munmap's own header, happening on schedule. 192 // Reuse is behaviour-identical: path is rebuilt from offset 0 and NUL-terminated every iteration, 193 // and szp is overwritten by ss_loadfile every iteration. Same hoist that fixed the tokenizer. 194 let path: *u8 = sys_mmap(512) 195 let szp: *i64 = sys_mmap(16) as *i64 196 // FAIL LOUD RATHER THAN WRITE THROUGH. A negative return is -errno; refusing here turns a silent 197 // memory-corrupting crash into a verdict a caller can read. 198 if (path as i64) <= 0 { wc9_puts("mmap failed for the path buffer\nverdict=FAIL\n" as *u8); return 12 } 199 if (szp as i64) <= 0 { wc9_puts("mmap failed for the size slot\nverdict=FAIL\n" as *u8); return 12 } 200 var total: i64 = 0 201 var s: i64 = 0 202 while s < ns { 203 var o: i64 = 0 204 o = ss_cat(path, o, prefix) 205 o = ss_cat(path, o, segs[s] as *u8) 206 o = ss_cat(path, o, ".docs" as *u8) 207 path[o] = 0 as u8 208 bptrs[s] = ss_loadfile(path, szp, 1) as i64 // mmap-open the input segments: sequential read, low RSS 209 bszs[s] = szp[0] 210 if bszs[s] < 0 { bszs[s] = 0 } 211 total = total + bszs[s] 212 s = s + 1 213 } 214 215 // RANGE-RUN SELECTION (2026-08-10; see the RANGE FOLD header note). Newest-first: find the newest 216 // contiguous run of >=2 NON-FAT segments whose total .docs bytes fit the budget. Fat segments are 217 // skipped over (they neither join a run nor end the search), so runs BEHIND a fat merged tail are 218 // reachable -- the exact population the suffix rule stranded. flo..fhi inclusive is the run. 219 // MAX-COUNT WINDOW, not newest-first (2026-08-10 v2). Newest-first starved convergence: the 220 // crawler always supplies a fresh 2-3 segment run at the tail, so every round folded THOSE and the 221 // ~800-segment old base below the fat products was never reached (measured: one round removed 2 222 // lines). Choosing the run with the MOST SEGMENTS under budget digs the base down first; once the 223 // whole shard is fat products, the newest small run wins ties and the organ degrades gracefully 224 // into tail maintenance. Two-pointer window, restarted at every fat boundary -- O(ns). 225 var fhi: i64 = 0 - 1 226 var flo: i64 = 0 227 var runbytes: i64 = 0 228 var bcnt: i64 = 0 229 var wlo: i64 = 0 230 var wrb: i64 = 0 231 var idx: i64 = 0 232 while idx < ns { 233 if bszs[idx] * WSC_FAT_DIV >= budget { 234 wlo = idx + 1 235 wrb = 0 236 } else { 237 wrb = wrb + bszs[idx] 238 while wrb > budget { wrb = wrb - bszs[wlo]; wlo = wlo + 1 } 239 let wcnt: i64 = idx - wlo + 1 240 if wcnt >= 2 { if wcnt >= bcnt { bcnt = wcnt; fhi = idx; flo = wlo; runbytes = wrb } } 241 } 242 idx = idx + 1 243 } 244 if fhi < 0 { 245 wc9_puts("CONVERGED: no contiguous run of >=2 non-fat segments fits the budget -- nothing left to fold at this budget (streaming writer lifts this)\nverdict=PASS\n" as *u8) 246 return 0 247 } 248 wc9_puts("fold-run: lo=" as *u8); wc9_num(flo); wc9_puts(" hi=" as *u8); wc9_num(fhi); wc9_puts(" fold=" as *u8); wc9_num(fhi - flo + 1) 249 wc9_puts(" run-bytes=" as *u8); wc9_num(runbytes); wc9_puts(" total-bytes=" as *u8); wc9_num(total); wc9_puts("\n" as *u8) 250 251 // PASS 1 over the RUN ONLY: count real entries (record framing walk, no fixed guess) 252 var nent: i64 = 0 253 var s0: i64 = flo 254 while s0 <= fhi { 255 let b0: *u8 = bptrs[s0] as *u8 256 let sz0: i64 = bszs[s0] 257 var i0: i64 = 0 258 while i0 + 9 <= sz0 { 259 let kl0: i64 = ss_r32(b0, i0 + 1) 260 let vl0: i64 = ss_r32(b0, i0 + 5 + kl0) 261 i0 = i0 + 5 + kl0 + 4 + vl0 262 nent = nent + 1 263 } 264 s0 = s0 + 1 265 } 266 if nent > WSC_MAXENT { wc9_puts("entry count exceeds WSC_MAXENT sanity ceiling\nverdict=FAIL\n" as *u8); return 2 } 267 let maxk: i64 = nent + 16 268 // hash table = smallest power of two >= 4x entries (load factor <= 0.25) 269 var hsize: i64 = WSC_MAGIC_1048576 270 var hbits: i64 = 20 271 while hsize < nent * 4 { hsize = hsize * 2; hbits = hbits + 1 } 272 let hshift: i64 = 63 - hbits 273 let hmask: i64 = hsize - 1 274 wc9_puts("pass1 entries=" as *u8); wc9_num(nent); wc9_puts(" hash-slots=" as *u8); wc9_num(hsize); wc9_puts("\n" as *u8) 275 276 // entry tables (dense) + hash slot -> entry-index (+1 so 0 = empty) 277 let tkp: *i64 = sys_mmap(8 * maxk) as *i64 278 let tkl: *i64 = sys_mmap(8 * maxk) as *i64 279 let tkind: *i64 = sys_mmap(8 * maxk) as *i64 280 let tvp: *i64 = sys_mmap(8 * maxk) as *i64 281 let tvl: *i64 = sys_mmap(8 * maxk) as *i64 282 let hslot: *i64 = sys_mmap(8 * hsize) as *i64 283 var nk: i64 = 0 284 var entries: i64 = 0 285 s = flo 286 while s <= fhi { 287 let b: *u8 = bptrs[s] as *u8 288 let sz: i64 = bszs[s] 289 var i: i64 = 0 290 while i + 9 <= sz { 291 let kind: i64 = b[i] 292 let kl: i64 = ss_r32(b, i + 1) 293 let koff: i64 = i + 5 294 let vl: i64 = ss_r32(b, koff + kl) 295 let voff: i64 = koff + kl + 4 296 let kp: *u8 = (b as i64 + koff) as *u8 297 // hash-probe for this key (byte-verify on hit; O(1) expected) 298 var slot: i64 = wsc_slot(kp, kl, hshift, hmask) 299 var hit: i64 = 0 - 1 300 var probing: i64 = 1 301 while probing == 1 { 302 let e: i64 = hslot[slot] 303 if e == 0 { probing = 0 } else { 304 if ss_kcmp(tkp[e - 1] as *u8, tkl[e - 1], kp, kl) == 0 { hit = e - 1; probing = 0 } else { 305 slot = (slot + 1) & hmask 306 } 307 } 308 } 309 if hit < 0 { 310 if nk >= maxk { wc9_puts("key table overflow (impossible: data-driven bound)\nverdict=FAIL\n" as *u8); return 3 } 311 hit = nk 312 hslot[slot] = nk + 1 313 nk = nk + 1 314 } 315 tkp[hit] = kp as i64 316 tkl[hit] = kl 317 tkind[hit] = kind 318 tvp[hit] = (b as i64) + voff 319 tvl[hit] = vl 320 entries = entries + 1 321 i = voff + vl 322 } 323 s = s + 1 324 } 325 wc9_puts("entries=" as *u8); wc9_num(entries); wc9_puts(" live-keys=" as *u8); wc9_num(nk) 326 wc9_puts(" run-bytes-in=" as *u8); wc9_num(runbytes); wc9_puts("\n" as *u8) 327 328 // merged segment (latest per key, insertion order = first-seen key order, matching ss_compact) 329 let w: *i64 = ss_begin_cap(runbytes + WSC_MAGIC_65536) 330 var t2: i64 = 0 331 while t2 < nk { 332 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 } 333 t2 = t2 + 1 334 } 335 wc9_puts("merge-write: emitting seg (docs+idx+pos+imp)...\n" as *u8) 336 if ss_write_seg(prefix, w, segid) != 0 { wc9_puts("segment write failed\nverdict=FAIL\n" as *u8); return 5 } 337 wc9_puts("merged segment written\n" as *u8) 338 339 // archive retired names BEFORE the swap (crash-safe ordering, identical to ss_compact) 340 let ap: *u8 = sys_mmap(512) 341 var ao: i64 = 0 342 ao = ss_cat(ap, ao, prefix) 343 ao = ss_cat(ap, ao, "manifest-archive.txt" as *u8) 344 ap[ao] = 0 as u8 345 let afd: i64 = sys_openat_append(ap, 0x1a4) 346 if afd < 0 { wc9_puts("archive open failed\nverdict=FAIL\n" as *u8); return 6 } 347 s = flo 348 while s <= fhi { 349 let nm2: *u8 = segs[s] as *u8 350 sys_write(afd, nm2, ss_len(nm2)) 351 sys_write(afd, "\n" as *u8, 1) 352 s = s + 1 353 } 354 sys_fsync(afd) 355 sys_close(afd) 356 // RE-READ THE MANIFEST UNDER THE PLANE LOCK, immediately before the swap. Crawler commits take 357 // <prefix>slock, NOT plock, so rows CAN land while a fold runs -- the old rewrite silently dropped 358 // them, orphaning freshly crawled segments (a lost-row class the seq349 note warned about). 359 // Appended rows are strictly at the tail (nothing else rewrites while we hold plock: sibling 360 // compactors block on plock, commits only append), so carrying segs2[ns..ns2-1] closes the loss 361 // window to the instant between this read and the rename. 362 let segs2: *i64 = sys_mmap(8 * WSC_MAXSEGS) as *i64 363 var ns2: i64 = ss_manifest_cap(prefix, segs2, WSC_MAXSEGS) 364 if ns2 < ns { ns2 = ns } 365 // atomic manifest swap -> kept lines [0..flo-1] + merged + kept lines [fhi+1..ns-1] + any rows 366 // appended past our snapshot (order preserved everywhere; the merged segment sits WHERE THE RUN SAT) 367 let mf: *u8 = sys_mmap(512) 368 let mt: *u8 = sys_mmap(512) 369 var o2: i64 = 0 370 o2 = ss_cat(mf, o2, prefix) 371 o2 = ss_cat(mf, o2, "manifest.txt" as *u8) 372 mf[o2] = 0 as u8 373 o2 = 0 374 o2 = ss_cat(mt, o2, prefix) 375 o2 = ss_cat(mt, o2, "manifest.tmp" as *u8) 376 mt[o2] = 0 as u8 377 let nb: *u8 = sys_mmap(40 * (ns2 + 4) + 256) 378 var no: i64 = 0 379 var km: i64 = 0 380 while km < flo { 381 no = ss_cat(nb, no, segs[km] as *u8) 382 nb[no] = 10 as u8 383 no = no + 1 384 km = km + 1 385 } 386 no = ss_cat(nb, no, "seg-" as *u8) 387 no = ss_catn(nb, no, segid) 388 nb[no] = 10 as u8 389 no = no + 1 390 km = fhi + 1 391 while km < ns { 392 no = ss_cat(nb, no, segs[km] as *u8) 393 nb[no] = 10 as u8 394 no = no + 1 395 km = km + 1 396 } 397 var appended: i64 = 0 398 km = ns 399 while km < ns2 { 400 no = ss_cat(nb, no, segs2[km] as *u8) 401 nb[no] = 10 as u8 402 no = no + 1 403 km = km + 1 404 appended = appended + 1 405 } 406 if appended > 0 { wc9_puts("carried " as *u8); wc9_num(appended); wc9_puts(" manifest row(s) appended by concurrent commits during the fold\n" as *u8) } 407 if ss_writefile(mt, nb, no) != 0 { wc9_puts("manifest tmp write failed\nverdict=FAIL\n" as *u8); return 7 } 408 if sys_renameat(mt, mf) != 0 { wc9_puts("manifest swap failed\nverdict=FAIL\n" as *u8); return 8 } 409 ss_syncdir(prefix) 410 411 // POST-SWAP VERIFY: reopen; sampled live keys from the folded run must RESOLVE via ss_hget 412 // (stride 97 ~= 1% coverage floor + first/last always). STRICT byte-equality is only a valid 413 // oracle when the run was the manifest SUFFIX and nothing landed during the fold -- for a MIDDLE 414 // run, a kept NEWER segment legitimately shadows a folded key with different bytes, and treating 415 // that as corruption would fail every healthy middle fold. So: a MISS is always bad (a key present 416 // in the run must resolve to SOMETHING after the swap); a byte MISMATCH is bad only in strict mode, 417 // otherwise counted as shadowed and reported. ss_open2(...,1) = mmap open: the read-all ss_open 418 // pulls the WHOLE shard into anon RAM -- the same OOM class this rewrite exists to kill. 419 var strict: i64 = 1 420 if fhi != ns - 1 { strict = 0 } 421 if appended > 0 { strict = 0 } 422 let h2: *i64 = ss_open2(prefix, 1) 423 if (h2 as i64) == 0 { wc9_puts("VERIFY reopen failed\nverdict=FAIL\n" as *u8); return 9 } 424 let vp: *i64 = sys_mmap(16) as *i64 425 let vl2: *i64 = sys_mmap(16) as *i64 426 let kbuf: *u8 = sys_mmap(600) 427 var bad: i64 = 0 428 var shadowed: i64 = 0 429 var checked: i64 = 0 430 var t3: i64 = 0 431 while t3 < nk { 432 var pick: i64 = 0 433 if t3 == 0 { pick = 1 } 434 if t3 == nk - 1 { pick = 1 } 435 if (t3 % 97) == 0 { pick = 1 } 436 if pick == 1 { if tkind[t3] == 1 { if tkl[t3] < 590 { 437 let sp2: *u8 = tkp[t3] as *u8 438 var x: i64 = 0 439 while x < tkl[t3] { kbuf[x] = sp2[x]; x = x + 1 } 440 kbuf[tkl[t3]] = 0 as u8 441 checked = checked + 1 442 if ss_hget(h2, kbuf, vp, vl2) != 1 { bad = bad + 1 } else { 443 var mism: i64 = 0 444 if vl2[0] != tvl[t3] { mism = 1 } else { 445 let a2: *u8 = vp[0] as *u8 446 let b2: *u8 = tvp[t3] as *u8 447 var y: i64 = 0 448 while y < vl2[0] { if a2[y] != b2[y] { mism = 1; y = vl2[0] } else { y = y + 1 } } 449 } 450 if mism == 1 { if strict == 1 { bad = bad + 1 } else { shadowed = shadowed + 1 } } 451 } 452 } } } 453 t3 = t3 + 1 454 } 455 wc9_puts("COMPACT done: segments " as *u8); wc9_num(ns2); wc9_puts(" -> " as *u8); wc9_num(ns2 - (fhi - flo + 1) + 1); wc9_puts(" (folded run -> seg-" as *u8); wc9_num(segid) 456 wc9_puts(") live-keys=" as *u8); wc9_num(nk); wc9_puts(" strict=" as *u8); wc9_num(strict); wc9_puts(" shadowed=" as *u8); wc9_num(shadowed) 457 wc9_puts(" verify-checked=" as *u8); wc9_num(checked); wc9_puts(" verify-bad=" as *u8); wc9_num(bad); wc9_puts("\n" as *u8) 458 if bad > 0 { 459 wc9_puts("VERIFY FAILED -- restore <prefix>manifest.txt from the retired list in manifest-archive.txt\nverdict=FAIL\n" as *u8) 460 return 10 461 } 462 wc9_puts("verdict=PASS\n" as *u8) 463 return 0 464}