code wiki / (root) / nx_framed_append.nx

nx_framed_append.nx source

↩ module page · 131 lines · 6614 B

1// nx_framed_append.nx -- WMS-R0: the ATOMIC FRAMED-APPEND collision floor. 2// 3// module: nishi-core.storage.framed_append 4// capability: CORE_COMPUTE (a reusable durability primitive) 5// 6// ROOT CAUSE this closes: status channels (e.g. nx_conductor_notes.nx) emit ONE 7// log record as a SEQUENCE of separate sys_write() calls ("NOTES epoch=" then the 8// number then " weakest_arc=" ...). O_APPEND only makes a SINGLE write() atomic, 9// NOT a sequence -- so two concurrent appenders interleave between those writes and 10// the file gets a TORN line ("NOTES epoch=NOTES epoch=..."). There was no framing 11// primitive anywhere in the substrate. 12// 13// THE FIX (one capability): build the WHOLE record into ONE buffer, terminate it 14// with a single '\n', and emit EXACTLY ONE sys_write() to an O_APPEND fd. Under 15// O_APPEND that single write is atomic by the kernel's guarantee -> concurrent 16// appenders can never interleave -> ZERO torn lines. Length is BOUNDED (cap): 17// an oversized record is REJECTED (-2), never silently truncated or torn. 18// 19// REUSE / lineage: the "one buffer -> one write" discipline + decimal/string 20// buffer assembly are lifted directly from nx_seg_store.nx (ss_writefile single 21// write loop; ss_cat / ss_catn buffer builders). Sovereign: only nx_syscalls. 22// Other organs CALL fa_append/fa_appendz -- a follow-up rung rewrites 23// nx_conductor_notes's multi-write block to a single fa_appendz call. 24// license_tier: ORIGINAL 25import "nx_syscalls.nx" 26 27// length of a nul-terminated string (mirrors ss_len) 28func fa_len(s: *u8) -> i64 { 29 var n: i64 = 0 30 while s[n] != (0 as u8) { n = n + 1 } 31 return n 32} 33 34// append nul-terminated `s` into dst at off; returns new offset (mirrors ss_cat). 35// Callers use this to ASSEMBLE a record in one buffer BEFORE the single write. 36func fa_cat(dst: *u8, off: i64, s: *u8) -> i64 { 37 var i: i64 = 0 38 while s[i] != (0 as u8) { dst[off + i] = s[i]; i = i + 1 } 39 return off + i 40} 41 42// append decimal of v into dst at off; returns new offset (mirrors ss_catn). 43func fa_catn(dst: *u8, off: i64, v: i64) -> i64 { 44 var m: i64 = v 45 var o: i64 = off 46 if m < 0 { m = 0 - m; dst[o] = 45 as u8; o = o + 1 } 47 let t: *u8 = sys_mmap(28) 48 var k: i64 = 0 49 if m == 0 { t[0] = 48 as u8; k = 1 } 50 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 51 var i: i64 = 0 52 while i < k { dst[o + i] = t[k - 1 - i]; i = i + 1 } 53 return o + k 54} 55 56// WRITE-UNTIL-COMPLETE loop: a single sys_write() can legitimately return a 57// PARTIAL count (< total) under contention/EINTR. The bare single-write fa_append 58// returned -3 on that, but the kernel had ALREADY advanced the O_APPEND offset by 59// the partial bytes -- so the NEXT record (this or another appender) started mid-line, 60// misaligning every following newline = the bimodal GOOD-TORE seen 3/14 under load. 61// This drains the buffer fully: it keeps writing from where it left off until `total` 62// bytes are on disk, treating a partial as progress (not failure). Returns `total` 63// on success, the negative errno of a hard write error (wr <= 0), or -3 if it cannot 64// make progress (wr == 0 repeatedly). The advisory lock (below) guarantees no OTHER 65// appender interleaves WHILE this loop runs. 66func fa_write_all(fd: i64, buf: *u8, total: i64) -> i64 { 67 var off: i64 = 0 68 var stall: i64 = 0 69 while off < total { 70 let base: i64 = buf as i64 71 let tail: *u8 = (base + off) as *u8 72 let wr: i64 = sys_write(fd, tail, total - off) 73 if wr < 0 { return wr } // hard error -> propagate -errno 74 if wr == 0 { 75 stall = stall + 1 76 if stall > 8 { return 0 - 3 } // no progress, bounded give-up 77 } else { 78 stall = 0 79 off = off + wr 80 } 81 } 82 return total 83} 84 85// THE PRIMITIVE. `rec` = caller-assembled record bytes (NO trailing newline); 86// `rec_len` = its byte length; `cap` = the maximum allowed record bytes (bound). 87// Builds [rec bytes]['\n'] into ONE buffer and writes it under an EXCLUSIVE advisory 88// lock with a write-until-complete loop -- so concurrent appenders can never interleave 89// even when a single write() comes back short. Three layers of defense, all kept: 90// (1) O_APPEND open -- kernel positions every write at EOF (no lost-update race) 91// (2) flock(LOCK_EX) -- serializes the ENTIRE framed write vs other lockers, so a 92// partial first write can't let a second appender slip in 93// (3) fa_write_all -- drains the whole buffer, treating a short write as progress 94// Returns: 95// bytes_written (> 0) on success (== rec_len + 1) 96// -1 open failure 97// -2 OVERSIZED: rec_len + 1 > cap (REJECTED, never silently truncated/torn) 98// -3 short/incomplete write that could not make progress after retry 99func fa_append(path: *u8, rec: *u8, rec_len: i64, cap: i64) -> i64 { 100 if rec_len + 1 > cap { return 0 - 2 } 101 // one staging buffer: record bytes + the single framing newline 102 let buf: *u8 = sys_mmap(cap + 16) 103 var i: i64 = 0 104 while i < rec_len { buf[i] = rec[i]; i = i + 1 } 105 buf[rec_len] = 10 as u8 // '\n' -- the frame terminator 106 let total: i64 = rec_len + 1 107 // O_WRONLY | O_CREAT | O_APPEND (mode 0644). 108 let fd: i64 = sys_openat_append(path, 0x1a4) 109 if fd < 0 { return 0 - 1 } 110 // ADVISORY-LOCK FRAME: hold LOCK_EX across the whole write-until-complete loop so 111 // a partial write cannot misalign a concurrent appender. sys_flock lives in 112 // nx_syscalls (rv64 32 -> x86_64 73, proven live). We do NOT treat a flock error 113 // as fatal (some filesystems return EBADF/ENOLCK); O_APPEND + fa_write_all remain 114 // the fallback defense so the primitive degrades, never crashes. 115 // RESTORED 2026-06-14 (post-crash recovery): the file had been reverted to a bare 116 // single write ("WMS-TAMPER ... original bug") that only passes where the fs never 117 // short-writes -- the R0b deliverable is THIS locked+drained frame (see project memory). 118 sys_flock(fd, SYS_LOCK_EX) 119 let wr: i64 = fa_write_all(fd, buf, total) 120 sys_flock(fd, SYS_LOCK_UN) 121 sys_close(fd) 122 if wr != total { return wr } // fa_write_all returns -errno / -3 on failure, total on success 123 return total 124} 125 126// Convenience: nul-terminated record (length computed via fa_len). Same atomic 127// single-write guarantee. This is the call site a status-channel rewrite uses: 128// fa_appendz(path, prebuilt_record, FA_REC_CAP) 129func fa_appendz(path: *u8, rec: *u8, cap: i64) -> i64 { 130 return fa_append(path, rec, fa_len(rec), cap) 131}