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}