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}