code wiki / (root) / nx_registry.nx

nx_registry.nx source

↩ module page · 197 lines · 10978 B

1// nx_registry.nx -- canonical generic seg_store keyed registry (put / get / enumerate), parameterized by a key-prefix 2// and an index key. Append-only / immutable (history kept). One commit advances both the record and the index, so 3// enumeration is atomic. The blocklist store + the tool registry are the same shape; this is the ONE implementation 4// they share (nx_blocklist_store can fold onto this later). Importable (no main). license_tier: ORIGINAL 5import "nx_seg_store.nx" 6import "nx_store_seed_lib.nx" // sts_lock / sts_unlock -- the plane lock the sts_ writer family already uses 7 8func reg_cat(dst: *u8, off: i64, s: *u8) -> i64 { var i: i64=0; while s[i]!=(0 as u8){ dst[off+i]=s[i]; i=i+1 } return off+i } 9func reg_strlen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n } 10 11// key = kp + id (NUL-terminated) into kbuf 12func reg_key(kbuf: *u8, kp: *u8, id: *u8) -> i64 { 13 var o: i64 = reg_cat(kbuf, 0, kp) 14 o = reg_cat(kbuf, o, id) 15 kbuf[o] = 0 as u8 16 return o 17} 18 19// is `id` (idlen) already a line in newline-separated buf[0..n)? 20func reg_id_present(buf: *u8, n: i64, id: *u8, idlen: i64) -> i64 { 21 var ls: i64=0; var i: i64=0 22 while i <= n { 23 var eol: i64=0 24 if i==n { eol=1 } else { if buf[i]==10 as u8 { eol=1 } } 25 if eol==1 { 26 if i-ls==idlen { var m: i64=1; var c: i64=0; while c<idlen { if buf[ls+c]!=id[c] { m=0 } c=c+1 } if m==1 { return 1 } } 27 ls=i+1 28 } 29 i=i+1 30 } 31 return 0 32} 33 34// put record under <kp><id>; advance the <idxkey> index (newline-separated ids) in the SAME commit (additive). 35func reg_put_x(prefix: *u8, kp: *u8, idxkey: *u8, id: *u8, record: *u8, reclen: i64, do_sync: i64) -> i64 { 36 // ★PLANE LOCK (2026-07-31). This function is a READ-MODIFY-WRITE over a store where -- per the 37 // note below -- EVERY REG SEGMENT CARRIES THE FULL INDEX, and it ran with NO LOCK AT ALL. Two 38 // concurrent puts each ss_get index version N, each append only their own id, and each write an 39 // N+1: one addition is silently lost. That is the SSOT-registration-loss class, and it is the 40 // measured cause of intermittent gate flakes (a closed accounting period reading OPEN ~12pct of 41 // runs, nx_coa_gate C8) because coa_period_close commits through here. 42 // WHY THIS LOCK AND NOT A NEW ONE: <prefix>plock is the SAME file the sts_ writer family takes 43 // (nx_store_seed_lib sts_lock). nx_seg_store itself contains ZERO flock calls across 261 44 // ss_commit callers, so the two families shared planes while only one serialised. Same lock = 45 // both families finally serialise. 46 // SAFE FROM SELF-DEADLOCK, VERIFIED NOT ASSUMED: flock(2) is per OPEN FILE DESCRIPTION, so a 47 // caller already holding <prefix>plock would hang against itself -- that is the documented trap 48 // that kept a lock OUT of sts_seed (nx_debt/nx_ecomat_put/nx_frontier_put pre-lock and call it). 49 // Checked all 52 reg_put callers on 2026-07-31: ZERO take a lock, so no caller can self-deadlock. 50 // NON-REGRESSIVE ON LOCK FAILURE: sts_lock already reports loudly and returns -1; we proceed 51 // instead of refusing, so a plock that cannot be opened leaves behaviour exactly as it was today. 52 let lk: i64 = sts_lock(prefix) 53 let kbuf: *u8 = sys_mmap(512) 54 reg_key(kbuf, kp, id) 55 let ipo: *i64 = sys_mmap(16) as *i64 56 let ilo: *i64 = sys_mmap(16) as *i64 57 // Read the index FIRST so buffers below are sized to NEED: the old fixed 58 // 1MiB idxbuf silently overflowed past ~15k ids (latent corruption for any 59 // large registry) and the old fixed-1MiB writer let ss_add fail-close 60 // UNCHECKED once record+index outgrew it (a commit missing its record). 61 var have: i64 = 0 62 if ss_get(prefix, idxkey, ipo, ilo) >= 0 { have = ilo[0] } 63 let nl: i64 = reg_strlen(id) 64 let idxbuf: *u8 = sys_mmap(have + nl + 16) 65 var ilen: i64 = 0 66 if have > 0 { 67 let src: *u8 = ipo[0] as *u8 68 var c: i64=0; while c<have { idxbuf[ilen]=src[c]; ilen=ilen+1; c=c+1 } 69 } 70 let already: i64 = reg_id_present(idxbuf, ilen, id, nl) 71 if already == 1 { 72 // CONTENT-IDEMPOTENT (2026-07-30): a byte-identical put of an already-indexed id must NOT 73 // mint a segment -- every reg segment carries the FULL index, so no-change re-registers 74 // (reconcile beats, register campaigns) grew the toolreg plane to 1355 segments and made 75 // every ss_get walk all of them (the tools/list 10s-per-page outage). The store itself now 76 // refuses to grow on a no-op: same bytes -> return success, zero writes. 77 let cpo: *i64 = sys_mmap(16) as *i64 78 let clo: *i64 = sys_mmap(16) as *i64 79 if ss_get(prefix, kbuf, cpo, clo) == 1 { if clo[0] == reclen { 80 let cur: *u8 = cpo[0] as *u8 81 var same: i64 = 1 82 var q: i64 = 0 83 while q < reclen { if cur[q] != record[q] { same = 0; q = reclen } else { q = q + 1 } } 84 if same == 1 { sts_unlock(lk); return 0 } 85 } } 86 } 87 if already == 0 { 88 var c2: i64=0; while c2<nl { idxbuf[ilen]=id[c2]; ilen=ilen+1; c2=c2+1 } 89 idxbuf[ilen]=10 as u8; ilen=ilen+1 90 } 91 // CANONICAL writer-side id (uncapped max+1): count-as-segid re-clobbers 92 // after compaction or past any cap (the proven toolreg seg-256 class) 93 let segid: i64 = ss_next_segid(prefix) 94 let w: *i64 = ss_begin_cap(reclen + ilen + 4096) 95 var bad: i64 = 0 96 if ss_add(w, 1, kbuf, record, reclen) < 0 { bad = 1 } 97 if ss_add(w, 1, idxkey, idxbuf, ilen) < 0 { bad = 1 } 98 if bad == 1 { sts_unlock(lk); return 0 - 1 } 99 var rc: i64 = 0 100 if do_sync == 1 { rc = ss_commit(prefix, w, segid) } 101 if do_sync != 1 { rc = ss_commit_deferred(prefix, w, segid) } 102 sts_unlock(lk) 103 return rc 104} 105 106// ---- TWO ENTRY POINTS, ONE BODY (2026-08-01 ws=legal, profile-driven, debt 1785610685) ------------ 107// PROFILED not guessed (nx_regprof_test, fresh plane): lock_cycle=35us, ss_get_absent=19us, 108// ss_get@20seg=371us, reg_put=129335us steady-state. The lock is 0.03% of a write and the read path 109// 0.3%, and cost FALLS as segments accumulate -- so neither the plane lock nor the full-index-per- 110// segment shape is the cost. What remains is ONE DIRECTORY FSYNC PER ROW inside ss_commit. 111// 112// u2605A transaction that appends N rows needs ONE durability barrier, not N. reg_put_x is the single 113// shared body so the two paths CANNOT DRIFT -- the fork in nx_gate_verdict (NAS copy grew functions 114// the laptop copy lacked) is the failure this avoids by construction. 115 116// u2605UNCHANGED CONTRACT. Durable per row, exactly as before. All 52 existing callers keep this 117// behaviour byte-for-byte and none of them had to be touched. 118func reg_put(prefix: *u8, kp: *u8, idxkey: *u8, id: *u8, record: *u8, reclen: i64) -> i64 { 119 return reg_put_x(prefix, kp, idxkey, id, record, reclen, 1) 120} 121 122// u2605BATCHED. Same row, same visibility -- the segment is written and the manifest rename has already 123// happened, so a reader sees the row IMMEDIATELY. ONLY the power-loss barrier is deferred, and the 124// caller owes exactly one reg_batch_sync at the end. 125// u26a0USE reg_put FOR A ROW THAT MUST SURVIVE A CRASH ON ITS OWN. Use this only inside a batch you will 126// sync: a crash between the last deferred put and the sync can lose the tail of that batch. That is 127// the trade a batch wants and the trade a lone critical write must never make. 128func reg_put_deferred(prefix: *u8, kp: *u8, idxkey: *u8, id: *u8, record: *u8, reclen: i64) -> i64 { 129 return reg_put_x(prefix, kp, idxkey, id, record, reclen, 0) 130} 131 132// u2605THE BARRIER THE BATCH OWES. Idempotent and cheap to over-call -- when in doubt, call it. 133func reg_batch_sync(prefix: *u8) -> i64 { 134 return ss_sync_now(prefix) 135} 136 137// current record of <kp><id> -> ptrout[0]/lenout[0]; 1=found, 0=tombstoned, -1=absent 138func reg_get(prefix: *u8, kp: *u8, id: *u8, ptrout: *i64, lenout: *i64) -> i64 { 139 let kbuf: *u8 = sys_mmap(512) 140 reg_key(kbuf, kp, id) 141 return ss_get(prefix, kbuf, ptrout, lenout) 142} 143 144// ---- INDEX ENUMERATION --------------------------------------------------- 145// ROOT-FIXED 2026-07-31. The original reg_index copied ss_get's ilo[0] bytes 146// into the caller's buffer and took NO capacity argument -- so the signature 147// itself could not express safety, and every one of its 18 call sites was an 148// unbounded write into a fixed mapping. 149// 150// MEASURED, not inferred: the live mvault mv:ids index had grown to 10-12 MiB 151// (152077 record versions) against a 4 MiB buffer in nx_mvault do_stats -- so 152// the vault's OWN RULER segfaulted and returned empty -- and against an 8 MiB 153// buffer in nx_vault_gateway vgw_serve_catalog, meaning the browse page was 154// already past its cliff too, not approaching it. reg_put was sized-to-need on 155// the WRITE side by an earlier fix; the READ side was never done. 156// 157// A guard that cannot be satisfied produces a bypass, not safety -- so the 158// unsafe arity is GONE rather than deprecated. Removing it is a COMPILE-BREAK 159// migration on purpose: every caller must state its capacity or borrow, and no 160// call site can silently survive. Callers pick one of: 161// reg_index_open zero-copy borrow of the store's own mapping (PREFERRED -- 162// enumeration is read-only and needs no private copy at all, 163// so it has no size cliff by construction) 164// reg_index_len byte length, so a caller can size a buffer to NEED 165// reg_index bounded copy that REFUSES rather than truncating 166// Silent truncation would be the SAME class of defect as the overflow: a 167// consumer that cannot tell a complete answer from a partial one. 168 169// Borrow the index in place: ptrout[0]/lenout[0] point INTO the store mapping. 170// Read-only and valid while the mapping is held. 1=found, 0=absent/empty. 171func reg_index_open(prefix: *u8, idxkey: *u8, ptrout: *i64, lenout: *i64) -> i64 { 172 if ss_get(prefix, idxkey, ptrout, lenout) < 0 { ptrout[0]=0; lenout[0]=0; return 0 } 173 return 1 174} 175 176// Byte length of the index WITHOUT copying it (0 if absent). 177func reg_index_len(prefix: *u8, idxkey: *u8) -> i64 { 178 let ipo: *i64 = sys_mmap(16) as *i64 179 let ilo: *i64 = sys_mmap(16) as *i64 180 if ss_get(prefix, idxkey, ipo, ilo) < 0 { return 0 } 181 return ilo[0] 182} 183 184// Bounded copy of the newline-separated id index into idxbuf, whose capacity 185// the caller MUST state. Returns bytes copied, 0 if absent, or -1 if the index 186// does NOT FIT -- it refuses and copies nothing, because a short read that 187// looked successful would let a caller report a partial catalogue as complete. 188func reg_index(prefix: *u8, idxkey: *u8, idxbuf: *u8, cap: i64) -> i64 { 189 let ipo: *i64 = sys_mmap(16) as *i64 190 let ilo: *i64 = sys_mmap(16) as *i64 191 if ss_get(prefix, idxkey, ipo, ilo) < 0 { return 0 } 192 let n: i64 = ilo[0] 193 if n > cap { return 0 - 1 } 194 let src: *u8 = ipo[0] as *u8 195 var i: i64=0; while i<n { idxbuf[i]=src[i]; i=i+1 } 196 return n 197}