nx_store_seed_lib.nx source
↩ module page · 455 lines · 25835 B
1// nx_store_seed_lib.nx -- THE canonical GENERIC store-seed codec (operator 2026-07-17: unify the
2// ~30 bespoke per-plane seeders). Migrates ANY flat staging buffer (# and blank lines skipped) into
3// ANY sovereign seg-store prefix as records key "q:<seq>"=row + "q:n"=count, and reconstructs a
4// newline-joined buffer byte-identical to the flat body. Prefix is a PARAMETER (gates isolate).
5// This is nx_frontier_store's frs_* promoted to the shared home; frontier + commontask delegate here.
6// license_tier: ORIGINAL No hw writes (Rule 26).
7import "nx_seg_store.nx"
8import "nx_syscalls.nx"
9import "nx_record_tsv.nx" // rtv_to_tsv + nxr_count -- the compatibility view (seq1326 rung 3)
10
11const STS_WCAP: i64 = 8388608 // RETIRED as the writer cap (see sts_seed); kept only for callers that still name it
12const STS_ROWOVH: i64 = 32 // per-entry seg-store header: kind + u32 klen + u32 vlen, with margin
13const STS_WSLACK: i64 = 65536 // count row + commit slack
14// ---- PLANE LOCK (seq1544 follow-on) -------------------------------------------------------------
15// MEASURED 2026-07-30: 55 files call sts_seed; only THREE take any lock. sts_seed is LOAD-ALL /
16// WRITE-ALL -- it REPLACES the whole plane from the caller's buffer -- so two unlocked writers that
17// overlap LOSE DATA, silently and by construction:
18// A loads (n rows) | B loads (n rows) | A commits (n+1) | B commits (n+1, WITHOUT A's row)
19// A's row is simply gone, and the next reader sees a declared/found mismatch it will blame on a
20// "prior writer dropped rows" -- which is exactly what it is, but the cause is the MISSING LOCK.
21//
22// WHY THIS IS A CALLER-HELD LOCK AND NOT AN sts_seed INTERNAL: the critical section is the whole
23// READ-MODIFY-WRITE (load -> mutate -> seed), not the commit alone. A lock taken inside sts_seed would
24// cover only the write half and leave the exact race above wide open. Worse, flock is per OPEN FILE
25// DESCRIPTION, so locking inside sts_seed would DEADLOCK the three callers that already hold
26// <prefix>plock on their own fd -- same process, different fd, blocks forever.
27//
28// ADDITIVE AND OPT-IN: nothing here changes existing behaviour. A caller wraps its RMW:
29// let lk: i64 = sts_lock(prefix) // <=0 means could not lock: refuse, do not proceed unlocked
30// ... sts_load_honest ... mutate ... sts_seed ...
31// sts_unlock(lk)
32// The lock file is <prefix>plock -- the SAME name nx_debt/nx_ecomat_put/nx_frontier_put already use
33// by hand, so adopting this composes with them instead of introducing a second, incompatible lock.
34// ─── DUPLICATE LOCK API REMOVED 2026-07-31 (ws=debt-intelligence) ─────────────────────────────────
35// This file defined sts_lock/sts_unlock TWICE -- here (the original) and again at the STS_LOCKMODE
36// block below. Harmless-looking while the compiler silently picked one; a HARD BUILD FAILURE for
37// EVERY consumer the moment the duplicate-definition guard went fail-closed on 2026-07-31, which is
38// how it was found (nx_debtcluster could not compile, and neither could any other store-seed
39// consumer -- nx_debtlive/nx_debt included).
40// THE SURVIVOR IS THE LOWER ONE, ON MERIT, NOT ON POSITION: it names the failing PATH on an open
41// error instead of letting "cannot lock" blame flock, and its sts_unlock issues an explicit
42// LOCK_UN before close rather than relying on close to drop the flock implicitly.
43// ★LAW: A DUPLICATE DEFINITION IS NOT A STYLE PROBLEM -- IT IS AN UNDECLARED COIN FLIP OVER WHICH
44// IMPLEMENTATION RUNS, AND IT SURVIVES EXACTLY UNTIL SOMETHING STARTS CHECKING.
45
46const STS_HASH: i64 = 35 // '#'
47const STS_NL: i64 = 10 // '\n'
48const STS_Q: i64 = 113 // 'q'
49const STS_COLON: i64 = 58 // ':'
50const STS_ZERO: i64 = 48 // '0'
51const STS_NINE: i64 = 57 // '9'
52const STS_KIND_LIVE: i64 = 1 // seg-store live-record kind (2 = tombstone)
53const STS_KEYCAP: i64 = 64
54const STS_NUMCAP: i64 = 28
55const STS_OUTCAP: i64 = 16
56const STS_BASE10: i64 = 10
57const STS_KPFX: i64 = 2 // "q:" prefix length
58const STS_EXIT_MMAP: i64 = 9
59const STS_MMAP_ERRMAX: i64 = 4096
60const STS_PROBE_WINDOW: i64 = 4096 // detect-only probe span past the declared q:n
61const STS_PROBE_MISS_RUN: i64 = 64 // consecutive misses that end the probe (sparse-tail guard)
62const STS_ERRFD: i64 = 2
63
64// CHECKED allocator (seq350, INCIDENT-0720): under host memory exhaustion sys_mmap returns an
65// error code in the top page; every unchecked caller then writes to it and SEGFAULTS (observed at
66// 0xfffffffffffffff4 = -ENOMEM, with multi-GB cores). This is the SHARED home of the seg-store
67// reader family (nx_debt/nx_debt_hygiene/nx_ws_cycle/nx_sweep_daemon all load through here), so the
68// check lives here ONCE and every member inherits it on its next rebuild. Fail LOUD with a
69// structured constant message (allocation-free by construction), never a segfault.
70func sts_werr(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(STS_ERRFD, s, n); return 0 }
71func sts_mm(n: i64) -> *u8 {
72 let p: *u8 = sys_mmap(n)
73 let v: i64 = p as i64
74 if v < 0 { if v > (0 - STS_MMAP_ERRMAX) {
75 sts_werr("STORE-RED {\"error\":\"mmap-fail\",\"lib\":\"nx_store_seed_lib\",\"hint\":\"host memory exhausted; see debt seq350 / INCIDENT-0720\"}\n" as *u8)
76 sys_exit(STS_EXIT_MMAP)
77 } }
78 return p
79}
80
81const STS_NXR_HDR: i64 = 6
82const STS_NXR_N: i64 = 78
83const STS_NXR_X: i64 = 88
84const STS_NXR_R: i64 = 82
85const STS_NXR_1: i64 = 49
86
87// FORMAT AUTO-DETECT (seq1326 rung 3) -- MUST STAY IDENTICAL TO _hdl_build/nx_store_seed_lib.nx.
88// ⚠⚠⚠DO NOT DROP THIS BLOCK WHEN REWRITING THIS FILE. It was clobbered twice on 2026-07-30 (14:12:33 and
89// 14:22:33) by full-file writes from a concurrent session, each time returning the file to its pre-edit
90// 10199B. knowledge/store/riskreg- is MIGRATED TO NXR1 on disk, so WITHOUT this block nx_info_registry
91// reads that live plane as rows=[] total=54 malformed=54 and nx_store_put load returns shredded garbage.
92// It is purely ADDITIVE: the render path is unreachable for any value not starting with the NXR1 magic,
93// so it is provably inert on every legacy plane and costs other lanes nothing.
94// ⚠Run nx_libcheck before building anything that imports this file -- the runtime/ and _hdl_build/ copies
95// are EACH OTHER'S CONTROL, and divergence means half the tree will silently build old behaviour.
96func sts_emit_row(src: *u8, vl: i64, out: *u8, off: i64, cap: i64) -> i64 {
97 var o: i64 = off
98 if vl >= STS_NXR_HDR {
99 if src[0] == (STS_NXR_N as u8) { if src[1] == (STS_NXR_X as u8) {
100 if src[2] == (STS_NXR_R as u8) { if src[3] == (STS_NXR_1 as u8) {
101 let cols: i64 = nxr_count(src)
102 let n2: i64 = rtv_to_tsv(src, vl, cols, (out as i64 + o) as *u8, cap - o)
103 if n2 >= 0 { return o + n2 }
104 return o
105 } } } }
106 }
107 var t2: i64 = 0
108 while t2 < vl { if o < cap { out[o] = src[t2]; o = o + 1 } t2 = t2 + 1 }
109 return o
110}
111
112func sts_atoi(b: *u8, n: i64) -> i64 {
113 var v: i64 = 0
114 var i: i64 = 0
115 var go: i64 = 1
116 while i < n {
117 if go == 1 {
118 let c: i64 = b[i] as i64
119 if c >= STS_ZERO { if c <= STS_NINE { v = v * STS_BASE10 + (c - STS_ZERO) } else { go = 0 } } else { go = 0 }
120 }
121 i = i + 1
122 }
123 return v
124}
125func sts_rowkey(seq: i64, out: *u8) -> i64 {
126 out[0] = STS_Q as u8
127 out[1] = STS_COLON as u8
128 var o: i64 = ss_catn(out, STS_KPFX, seq)
129 out[o] = 0 as u8
130 return o
131}
132// migrate a flat buffer into the store at `prefix`; ONE commit. returns row count or -1.
133func sts_seed(prefix: *u8, buf: *u8, n: i64) -> i64 {
134 // WRITER CAPACITY IS DATA-DRIVEN, NOT A CEILING (2026-07-30). This was ss_begin_cap(STS_WCAP)
135 // with STS_WCAP a fixed 1MiB. sts_seed rewrites the WHOLE plane on every write, so the moment a
136 // plane outgrew 1MiB the trailing ss_add calls failed -- and their return value was DISCARDED --
137 // while the q:n count row still recorded the INTENDED total. The plane then declared more rows
138 // than it contained: silent, unbounded data loss that only surfaced later as DEBT-REFUSED
139 // lossy-load, with the debt board (the ecosystem ledger) frozen read-only.
140 // Size from the actual payload the way reg_put and ss_compact_cap already do: every byte of
141 // input, plus per-row key+header overhead, plus the count row and slack. A plane can now grow
142 // to whatever it needs; there is no number here for a future plane to outgrow.
143 var rowcount: i64 = 0
144 var scan: i64 = 0
145 while scan < n { if buf[scan] == (STS_NL as u8) { rowcount = rowcount + 1 } scan = scan + 1 }
146 // ⚠A SHRINK-REFUSAL GUARD WAS ATTEMPTED HERE 2026-08-02 AND DELIBERATELY REVERTED BEFORE PROMOTE.
147 // It would have refused any sts_seed whose input holds fewer rows than the plane already has --
148 // correct for the clobber class (dedupq- seq756, commontask- 08-02) but WRONG HERE, because a
149 // blast-radius read of the 118 call sites found LEGITIMATE shrinking writers that the guard would
150 // have broken, some catastrophically:
151 // * nx_plane_append ROLLBACK -- sts_seed(prefix, oldb, oldn) restores the PRE-append buffer after a
152 // failed write; refusing that strands the plane in the failed state (the guard would cause the
153 // very loss it exists to prevent).
154 // * nx_plane_repair -- repairing a corrupted plane legitimately produces FEWER rows.
155 // * nx_plane_append's own T1 NEGATIVE CONTROL deliberately raw-seeds to PROVE a raw seed destroys a
156 // plane (2 rows -> 1); refusing it turns a healthy liar-killed gate RED.
157 // * nx_store_put close/setcol paths rewrite the whole plane by design.
158 // THE GUARD BELONGS AT THE PLANE-WRITER LAYER, NOT IN THE SHARED PRIMITIVE: see debt 1785710884 --
159 // either a checked twin (sts_seed_checked) that intentional shrinkers opt out of, or the guard inside
160 // nx_store_put's put path only. The primitive stays raw and DOCUMENTED-DANGEROUS; nx_plane_append is
161 // already the one correct append path.
162 let needed: i64 = n + (rowcount + 2) * (STS_KEYCAP + STS_ROWOVH) + STS_WSLACK
163 let w: *i64 = ss_begin_cap(needed)
164 let key: *u8 = sts_mm(STS_KEYCAP)
165 let base: i64 = buf as i64
166 var seq: i64 = 0
167 var i: i64 = 0
168 while i < n {
169 var le: i64 = i
170 var s: i64 = 1
171 while s == 1 { if le >= n { s = 0 } else { if buf[le] == (STS_NL as u8) { s = 0 } else { le = le + 1 } } }
172 if le > i { if buf[i] != (STS_HASH as u8) {
173 sts_rowkey(seq, key)
174 // CHECKED: a dropped row used to be invisible here, which is what let q:n over-declare.
175 if ss_add(w, STS_KIND_LIVE, key, (base + i) as *u8, le - i) < 0 { return 0 - 2 }
176 seq = seq + 1
177 } }
178 i = le + 1
179 }
180 let cb: *u8 = sts_mm(STS_NUMCAP)
181 let cl: i64 = ss_catn(cb, 0, seq)
182 if ss_add(w, STS_KIND_LIVE, "q:n" as *u8, cb, cl) < 0 { return 0 - 2 }
183 let rc: i64 = ss_commit(prefix, w, ss_next_segid(prefix))
184 if rc != 0 { return 0 - 1 }
185 return seq
186}
187// ---------------------------------------------------------------------------------------------------
188// sts_append_fast -- O(1) APPEND OF ONE ROW. The seq724 amplification fix, living beside sts_seed so every
189// writer that already imports this lib can reach it without a new dependency.
190//
191// sts_seed REWRITES THE WHOLE PLANE. Measured 2026-07-30 on a 2000-row / 210,906-byte plane: appending one
192// row via re-seed wrote a 210,969-byte segment; this path wrote 47 BYTES. 4489x less. On dp-web-pub-
193// (1.5GB, 4,291,836 entries) that is 1.5GB rewritten versus about 50 bytes.
194//
195// IT IS NOT ONLY A SPEED FIX -- IT REMOVES TWO WHOLE DEFECT CLASSES BY CONSTRUCTION:
196// * PARTIAL-LOAD DROPS ROWS: a re-seed rebuilds the plane from whatever the loader could reach, so a
197// partial read silently deletes every row past the declared count (the lossy-plane class). An append
198// rewrites NOTHING, so an incomplete read cannot destroy a row it never saw.
199// * ORDINAL SHIFT: a re-seed RE-SEGMENTS rows (any row containing a newline splits), shifting every
200// ordinal after it -- which is how a positional seq issued before a re-seed later addresses a
201// DIFFERENT row. An append adds q:<n> and touches no existing key, so ordinals are stable.
202//
203// Correctness rests on properties the store already guarantees: segments are append-only and reads are
204// NEWEST-WINS per key; q:<n> is a key never written before; q:n is simply overwritten; and every reader
205// already walks q:0..q:n-1 ACROSS segments. NO READER CHANGES ARE REQUIRED.
206// Segment count grows by one per append BY DESIGN -- nx_store_fold_beat merges on the standing sweep and
207// nx_store_janitor_beat reclaims what folding supersedes. Cheap appends + background compaction is the LSM
208// shape this store was built for and that re-seeding defeats.
209// FAIL-CLOSED: a plane with no q:n is REFUSED (-1), never conjured. Returns the NEW row count, or -1/-2.
210func sts_append_fast(prefix: *u8, row: *u8, rowlen: i64) -> i64 {
211 if rowlen <= 0 { return 0 - 1 }
212 let pq: *i64 = sts_mm(STS_OUTCAP) as *i64
213 let lq: *i64 = sts_mm(STS_OUTCAP) as *i64
214 if ss_get(prefix, "q:n" as *u8, pq, lq) != 1 { return 0 - 1 }
215 let n: i64 = sts_atoi(pq[0] as *u8, lq[0])
216 let key: *u8 = sts_mm(STS_KEYCAP)
217 sts_rowkey(n, key)
218 let cb: *u8 = sts_mm(STS_NUMCAP)
219 let cl: i64 = ss_catn(cb, 0, n + 1)
220 let w: *i64 = ss_begin_cap(rowlen + STS_WSLACK)
221 if ss_add(w, STS_KIND_LIVE, key, row, rowlen) < 0 { return 0 - 2 }
222 if ss_add(w, STS_KIND_LIVE, "q:n" as *u8, cb, cl) < 0 { return 0 - 2 }
223 if ss_commit(prefix, w, ss_next_segid(prefix)) != 0 { return 0 - 2 }
224 return n + 1
225}
226
227// sts_append_fast IS STILL A READ-MODIFY-WRITE AND IT STILL LOSES ROWS UNDER CONCURRENCY.
228// Measured/reasoned 2026-07-30 (ws=sev-eater): it ss_get's q:n to learn n, then writes q:<n> and
229// q:n=n+1. Two concurrent appenders both read n, both write THE SAME q:<n> key -- and reads are
230// NEWEST-WINS per key, so one row does not merely fail to appear, it is silently OVERWRITTEN -- and
231// both then set q:n=n+1, so the count agrees with the loss and no guard ever notices. It is 4489x
232// cheaper than a re-seed and exactly as lossy; SPEED IS NOT SAFETY.
233//
234// THE LOCK IS DELIBERATELY *NOT* INSIDE sts_append_fast. Audited its callers first, and nx_debt
235// (nx_debt.nx:320) ALREADY HOLDS <prefix>plock via db_lock and then calls it on THAT SAME prefix --
236// flock is per open-file-description, so an internal lock would make nx_debt block against ITSELF
237// and hang the debt board for every seat. Same trap that rules out locking sts_seed.
238// ★LAW, twice proven in one file now: BEFORE LOCKING A SHARED PRIMITIVE, AUDIT WHO ALREADY HOLDS
239// THAT LOCK -- the callers doing it right are what the internal lock breaks first.
240//
241// So the safe variant is a WRAPPER. Callers that do NOT already hold the plane lock use this one;
242// callers that do (nx_debt, nx_frontier_put) keep calling the raw primitive and stay correct.
243// (the wrapper itself is defined at the END of this file, below sts_lock -- NishiLang resolves
244// identifiers in TEXTUAL order and a forward reference to sts_lock from here would not compile.)
245
246// read the store at `prefix` -> newline-joined row buffer. returns bytes.
247// seq356 (INCIDENT-0720 residual): the original body did one ss_get PER ROW, and every ss_get
248// re-opens + re-maps the whole seglist => O(rows x segments) file IO -- the amplification that
249// helped the reader family (nx_debt/nx_ws_cycle/...) OOM the host. NOW: ss_open ONCE ->
250// ss_hget per row (zero file IO per call; the proven WMS audit-scale pattern). Semantics
251// identical BY ORACLE: sts_load_slow below is the verbatim old body and the bulkload gate
252// proves byte-identical output on single-seg, multi-seg-supersede, and shrink planes.
253func sts_load(prefix: *u8, out: *u8, cap: i64) -> i64 {
254 // ss_open_cached, NOT ss_open (seq905/962 CLASS FIX, 2026-07-30). ss_open reads every live segment
255 // fully into anonymous RAM and nothing frees it, so a consumer that loads repeatedly leaks the WHOLE
256 // store per load -- the fingerprint behind ~23.6 GiB of anonymous address space across nine organs and
257 // the 92.3%-swap thrash. MEASURED 2026-07-30: 133 files reach ss_open but only 1 called ss_close, so
258 // the fix existed and was dark. This lib is a CHOKEPOINT -- 63 importers, and it is the exact reader
259 // family (nx_debt / nx_ws_cycle / ...) named above as having OOM'd the host -- so migrating the two
260 // opens here reaches far more consumers than migrating leaf call sites one at a time (rule 15).
261 // Cache is prefix-keyed with manifest (st_size, st_mtime) invalidation, so LIVE EDITS ARE STILL PICKED
262 // UP on the next call with no restart; you simply cannot leak what you never allocate twice.
263 // ⚠Safe here because these loaders are single-threaded and run to completion before the next call, so
264 // the handle's shared query-scratch arena is never re-entered. Do NOT blanket-alias ss_open itself.
265 let h: *i64 = ss_open_cached(prefix)
266 if (h as i64) == 0 { return 0 }
267 let pq: *i64 = sts_mm(STS_OUTCAP) as *i64
268 let lq: *i64 = sts_mm(STS_OUTCAP) as *i64
269 if ss_hget(h, "q:n" as *u8, pq, lq) != 1 { return 0 }
270 let cnt: i64 = sts_atoi(pq[0] as *u8, lq[0])
271 let key: *u8 = sts_mm(STS_KEYCAP)
272 let pr: *i64 = sts_mm(STS_OUTCAP) as *i64
273 let lr: *i64 = sts_mm(STS_OUTCAP) as *i64
274 var o: i64 = 0
275 var seq: i64 = 0
276 while seq < cnt {
277 sts_rowkey(seq, key)
278 if ss_hget(h, key, pr, lr) == 1 {
279 let src: *u8 = pr[0] as *u8
280 let vl: i64 = lr[0]
281 o = sts_emit_row(src, vl, out, o, cap)
282 if o < cap { out[o] = STS_NL as u8; o = o + 1 }
283 }
284 seq = seq + 1
285 }
286 return o
287}
288// HONEST LOADER (2026-07-25, ws=debt-instrument). sts_load trusts the q:n count key and stops
289// at it with NO error and NO flag, so a low/stale q:n SILENTLY truncates every reader that uses
290// it. MEASURED on the debt- plane: q:n resolved 125 while 629 segments held 734+ rows, and
291// nx_debt list reported total=125 AS FACT. Loads byte-identically to sts_load (which is left
292// UNTOUCHED for API-contract stability, rule 19) but additionally REPORTS what it could not
293// reach, so partial coverage can never again be presented as complete (honesty law L011).
294// flags[0]=declared q:n flags[1]=rows actually loaded flags[2]=rows reachable BEYOND q:n
295// DETECT-ONLY BY CONSTRUCTION: never loads a row past the declared count. sts_seed REASSIGNS
296// q:<i> on every commit, so rows past q:n belong to an OLDER seeding generation -- loading them
297// would duplicate live rows and revive eaten ones. Reporting the gap is safe; reconciling it is
298// a separate operation that must diff the generations.
299// SAFE ASYMMETRY it relies on: a too-LOW q:n silently truncates, a too-HIGH q:n is harmless on
300// read because ss_hget misses are skipped -- so probing past the count cannot corrupt.
301func sts_load_honest(prefix: *u8, out: *u8, cap: i64, flags: *i64) -> i64 {
302 flags[0] = 0
303 flags[1] = 0
304 flags[2] = 0
305 // ss_open_cached -- same seq905/962 class fix and same single-threaded-loader reasoning as sts_load.
306 let h: *i64 = ss_open_cached(prefix)
307 if (h as i64) == 0 { return 0 }
308 let pq: *i64 = sts_mm(STS_OUTCAP) as *i64
309 let lq: *i64 = sts_mm(STS_OUTCAP) as *i64
310 if ss_hget(h, "q:n" as *u8, pq, lq) != 1 { return 0 }
311 let cnt: i64 = sts_atoi(pq[0] as *u8, lq[0])
312 flags[0] = cnt
313 let key: *u8 = sts_mm(STS_KEYCAP)
314 let pr: *i64 = sts_mm(STS_OUTCAP) as *i64
315 let lr: *i64 = sts_mm(STS_OUTCAP) as *i64
316 var o: i64 = 0
317 var seq: i64 = 0
318 while seq < cnt {
319 sts_rowkey(seq, key)
320 if ss_hget(h, key, pr, lr) == 1 {
321 let src: *u8 = pr[0] as *u8
322 let vl: i64 = lr[0]
323 o = sts_emit_row(src, vl, out, o, cap)
324 if o < cap { out[o] = STS_NL as u8; o = o + 1 }
325 flags[1] = flags[1] + 1
326 }
327 seq = seq + 1
328 }
329 var beyond: i64 = 0
330 var miss: i64 = 0
331 var p: i64 = cnt
332 let lim: i64 = cnt + STS_PROBE_WINDOW
333 while p < lim {
334 if miss < STS_PROBE_MISS_RUN {
335 sts_rowkey(p, key)
336 if ss_hget(h, key, pr, lr) == 1 { beyond = beyond + 1; miss = 0 } else { miss = miss + 1 }
337 }
338 p = p + 1
339 }
340 flags[2] = beyond
341 return o
342}
343// ---- ATOMIC PLANE APPEND (root fix for seq1559, ws=sev-eater 2026-07-30) ----------------------
344// 55 files call sts_seed(); only THREE take a lock. sts_seed REPLACES the whole plane from the
345// caller's buffer, and the universal call shape is READ-MODIFY-WRITE:
346// n = sts_load(prefix, buf, cap); ...append a row...; sts_seed(prefix, buf, n)
347// Two overlapping unlocked writers interleave as A-loads / B-loads / A-commits n+1 /
348// B-commits n+1-WITHOUT-A. A's row is gone, silently, and the next reader's declared-vs-found
349// mismatch blames a prior writer for dropping rows -- true, but the wrong cause.
350//
351// WHY THE OBVIOUS FIX IS WRONG AND IS NOT DONE HERE: putting the lock INSIDE sts_seed would
352// DEADLOCK the three callers that already do it right. flock(2) locks are per OPEN FILE
353// DESCRIPTION, not per process, so nx_debt -- which already holds <prefix>plock via db_lock and
354// then calls sts_seed -- would block against ITSELF on a second fd of the same file. That trades
355// silent data loss for a hard hang of the debt board for every seat, which is strictly worse.
356// ★LAW: A CHOKEPOINT FIX MUST NOT ASSUME ITS CALLERS ARE ALL WRONG -- the ones already doing it
357// right are exactly what a naive fix breaks first.
358//
359// So sts_seed is left UNLOCKED and this is added ALONGSIDE it: the whole critical section as ONE
360// call. ★LAW: THE RIGHT PRIMITIVE IS THE CRITICAL SECTION, NOT THE LOCK -- a lock 52 callers must
361// remember to take is the same class of defect as the missing lock. Callers migrate to
362// sts_append_row and become correct by construction; nothing has to be remembered.
363//
364// The lock file is <prefix>plock -- deliberately THE SAME PATH nx_debt/nx_ecomat_put/
365// nx_frontier_put already use, so migrated and unmigrated writers share ONE lock domain. A new
366// lock file would have been easier and would have protected nothing.
367const STS_LOCK_EX: i64 = 2
368const STS_LOCK_UN: i64 = 8
369const STS_LOCKMODE: i64 = 420
370const STS_LOCKPATHCAP: i64 = 256
371const STS_ERR_LOCK: i64 = 0 - 3
372const STS_ERR_CAP: i64 = 0 - 4
373
374// Take the plane's exclusive lock. Returns the held fd, or -1 (LOUD: names the path, because
375// "cannot lock" alone has historically blamed flock when the cause was an open failure).
376func sts_lock(prefix: *u8) -> i64 {
377 let p: *u8 = sts_mm(STS_LOCKPATHCAP)
378 var o: i64 = ss_cat(p, 0, prefix)
379 o = ss_cat(p, o, "plock" as *u8)
380 p[o] = 0 as u8
381 let fd: i64 = sys_openat_append(p, STS_LOCKMODE)
382 if fd < 0 {
383 sts_werr("STS-LOCK open-failed (not flock) path=" as *u8)
384 sts_werr(p)
385 sts_werr("\n" as *u8)
386 return 0 - 1
387 }
388 sys_flock(fd, STS_LOCK_EX)
389 return fd
390}
391
392func sts_unlock(fd: i64) -> i64 {
393 if fd < 0 { return 0 }
394 sys_flock(fd, STS_LOCK_UN)
395 sys_close(fd)
396 return 0
397}
398
399// ATOMIC ROW APPEND: lock -> load -> append -> commit -> unlock, with no window in which another
400// writer can load a snapshot this one is about to invalidate. Returns sts_seed's row count, or
401// STS_ERR_LOCK / STS_ERR_CAP. Fail-closed: if the buffer cannot hold the appended row the plane is
402// left EXACTLY as it was rather than committed short (a truncated commit is the very loss this
403// exists to prevent).
404func sts_append_row(prefix: *u8, row: *u8, rowlen: i64, cap: i64) -> i64 {
405 let fd: i64 = sts_lock(prefix)
406 if fd < 0 { return STS_ERR_LOCK }
407 let buf: *u8 = sts_mm(cap)
408 var n: i64 = sts_load(prefix, buf, cap)
409 if n < 0 { n = 0 }
410 if n + rowlen + 1 > cap { sts_unlock(fd); return STS_ERR_CAP }
411 var i: i64 = 0
412 while i < rowlen { buf[n] = row[i]; n = n + 1; i = i + 1 }
413 buf[n] = STS_NL as u8
414 n = n + 1
415 let rc: i64 = sts_seed(prefix, buf, n)
416 sts_unlock(fd)
417 return rc
418}
419
420// Locked form of sts_append_fast, defined HERE because it needs sts_lock above it (textual order).
421// Use this from any caller that does NOT already hold the plane lock; callers that DO (nx_debt,
422// nx_frontier_put) must keep calling the raw sts_append_fast or they will deadlock against
423// themselves -- see the audit note beside sts_append_fast.
424func sts_append_fast_locked(prefix: *u8, row: *u8, rowlen: i64) -> i64 {
425 let fd: i64 = sts_lock(prefix)
426 if fd < 0 { return STS_ERR_LOCK }
427 let rc: i64 = sts_append_fast(prefix, row, rowlen)
428 sts_unlock(fd)
429 return rc
430}
431
432// the pre-seq356 body VERBATIM (one ss_get per row) -- kept as the equivalence ORACLE for
433// nx_sts_bulkload_gate; not for production use (the amplification lives here).
434func sts_load_slow(prefix: *u8, out: *u8, cap: i64) -> i64 {
435 let pq: *i64 = sts_mm(STS_OUTCAP) as *i64
436 let lq: *i64 = sts_mm(STS_OUTCAP) as *i64
437 if ss_get(prefix, "q:n" as *u8, pq, lq) != 1 { return 0 }
438 let cnt: i64 = sts_atoi(pq[0] as *u8, lq[0])
439 let key: *u8 = sts_mm(STS_KEYCAP)
440 let pr: *i64 = sts_mm(STS_OUTCAP) as *i64
441 let lr: *i64 = sts_mm(STS_OUTCAP) as *i64
442 var o: i64 = 0
443 var seq: i64 = 0
444 while seq < cnt {
445 sts_rowkey(seq, key)
446 if ss_get(prefix, key, pr, lr) == 1 {
447 let src: *u8 = pr[0] as *u8
448 let vl: i64 = lr[0]
449 o = sts_emit_row(src, vl, out, o, cap)
450 if o < cap { out[o] = STS_NL as u8; o = o + 1 }
451 }
452 seq = seq + 1
453 }
454 return o
455}