code wiki / (root) / nx_atomic_rewrite.nx

nx_atomic_rewrite.nx source

↩ module page · 141 lines · 6031 B

1// nx_atomic_rewrite.nx -- WMS-R3: a GENERIC, callable atomic whole-file rewrite 2// primitive. The reusable twin of the seg-store commit idiom (ss_writefile + 3// sys_renameat + ss_syncdir), lifted out of the segment/manifest model into a 4// plain (path, bytes, n) API so ANY whole-file rewriter (e.g. nx_reconcile's 5// assignment_queue.tsv rewrite, capability ladders, status snapshots) can stop 6// truncating-in-place. 7// 8// ROOT CAUSE this closes: an in-place rewrite opens O_WRONLY|O_CREAT|O_TRUNC 9// (0x241), which TRUNCATES the live file to 0 BEFORE the new bytes land, then 10// streams a write-loop. A crash anywhere in that loop leaves the live file 11// truncated/torn. O_APPEND atomicity does NOT help -- it only makes ONE 12// write() atomic, never a truncate-then-rewrite sequence. 13// 14// THE FIX (atomic_rewrite): stage the FULL new contents into a SIBLING temp 15// file (same directory => same filesystem => renameat(2) is atomic), fsync the 16// temp so its bytes are durable, then renameat(tmp -> path) -- the single 17// atomic COMMIT POINT (a concurrent reader sees the whole old file or the whole 18// new file, never a torn read), then fsync the parent directory so the rename 19// itself survives power loss. A crash BEFORE the rename leaves the live file 20// completely untouched (only the temp is partial, and the temp is never read). 21// 22// REUSE: idiom lifted verbatim from nx_seg_store.nx ss_writefile (write-loop + 23// sys_fsync), ss_syncdir (dir fsync), and the temp->rename commit discipline. 24// SOVEREIGN: nx_syscalls only; no gcc / 3rd-party. license_tier: ORIGINAL 25import "nx_syscalls.nx" 26const K_MAGIC_1024: i64 = 1024 27 28// strlen for null-terminated paths (callers pass C-strings). 29func ar_len(s: *u8) -> i64 { 30 var n: i64 = 0 31 while s[n] != (0 as u8) { n = n + 1 } 32 return n 33} 34 35// Build "<path>.nxtmp" into out (null-terminated); returns its length. Fixed 36// ".nxtmp" suffix keeps the temp a SIBLING of the target (same dir => same FS 37// => renameat is atomic; cross-FS rename is NOT atomic and would copy+unlink). 38// Contract: one writer per path at a time -- the rename is the serialization 39// point, so a fixed suffix is safe for the single-writer whole-file-rewrite 40// pattern (the exact shape of every victim). 41func ar_tmpname(path: *u8, out: *u8) -> i64 { 42 var i: i64 = 0 43 while path[i] != (0 as u8) { out[i] = path[i]; i = i + 1 } 44 let suf: *u8 = ".nxtmp" as *u8 45 var j: i64 = 0 46 while suf[j] != (0 as u8) { out[i] = suf[j]; i = i + 1; j = j + 1 } 47 out[i] = 0 as u8 48 return i 49} 50 51// fsync the directory holding `path` (lifted from ss_syncdir): scan to the last 52// '/', open that dir O_RDONLY, fsync it, close. No '/' => current directory. 53// Power-loss closure of the rename itself. Best-effort (returns rc). 54func ar_syncdir(path: *u8) -> i64 { 55 let d: *u8 = sys_mmap(512) 56 var last: i64 = 0 - 1 57 var i: i64 = 0 58 while path[i] != (0 as u8) { 59 if path[i] == (47 as u8) { last = i } 60 i = i + 1 61 } 62 if last < 0 { 63 d[0] = 46 as u8 64 d[1] = 0 as u8 65 } 66 if last >= 0 { 67 var t: i64 = 0 68 while t <= last { d[t] = path[t]; t = t + 1 } 69 d[t] = 0 as u8 70 } 71 let fd: i64 = sys_openat_rd(d) 72 if fd < 0 { return 0 - 1 } 73 let rc: i64 = sys_fsync(fd) 74 sys_close(fd) 75 return rc 76} 77 78// THE PRIMITIVE. Atomically replace the whole contents of `path` with the `n` 79// bytes at `bytes`. Returns 0 on success; negative on the failing step: 80// -1 open temp failed -2 short/failed write -3 rename (commit) failed. 81// Dir-sync is best-effort (post-commit power-loss hardening), never fails the 82// call. Steps: stage temp -> fsync temp -> RENAME (commit) -> fsync dir. 83func atomic_rewrite(path: *u8, bytes: *u8, n: i64) -> i64 { 84 let tmp: *u8 = sys_mmap(K_MAGIC_1024) 85 ar_tmpname(path, tmp) 86 let fd: i64 = sys_openat_wr(tmp, 0x1a4) 87 if fd < 0 { return 0 - 1 } 88 var off: i64 = 0 89 while off < n { 90 let wr: i64 = sys_write(fd, (bytes as i64 + off) as *u8, n - off) 91 if wr <= 0 { sys_close(fd); return 0 - 2 } 92 off = off + wr 93 } 94 sys_fsync(fd) 95 sys_close(fd) 96 // THE ATOMIC COMMIT POINT: a reader of `path` sees the whole old file 97 // until this returns, then the whole new file -- never a torn read. 98 if sys_renameat(tmp, path) != 0 { return 0 - 3 } 99 ar_syncdir(path) 100 return 0 101} 102 103// CRASH-INJECTION hook for the gate ONLY (twin of ss_crashwrite): stage the 104// temp + fsync but RETURN BEFORE the rename -- simulates death after staging, 105// before commit. A reader of `path` must STILL see the intact OLD file 106// (the temp is a sibling that no reader of `path` ever opens). 107func ar_crashwrite(path: *u8, bytes: *u8, n: i64) -> i64 { 108 let tmp: *u8 = sys_mmap(K_MAGIC_1024) 109 ar_tmpname(path, tmp) 110 let fd: i64 = sys_openat_wr(tmp, 0x1a4) 111 if fd < 0 { return 0 - 1 } 112 var off: i64 = 0 113 while off < n { 114 let wr: i64 = sys_write(fd, (bytes as i64 + off) as *u8, n - off) 115 if wr <= 0 { sys_close(fd); return 0 - 2 } 116 off = off + wr 117 } 118 sys_fsync(fd) 119 sys_close(fd) 120 // DELIBERATELY NO rename -- the staged temp never commits. 121 return 0 122} 123 124// NEGATIVE-CONTROL primitive: the OLD unguarded in-place rewrite. Opens the 125// LIVE path with O_TRUNC (truncates to 0 immediately), then writes ONLY the 126// first `n` bytes. The gate forks a child that calls this with a HALF count 127// and exits -- modelling a crash mid-write -- leaving the live file torn. 128// This is the wrong-variant that MUST tear so the gate can prove atomicity 129// actually matters (a neg-control that does not bite proves nothing). 130func ar_unsafe_inplace(path: *u8, bytes: *u8, n: i64) -> i64 { 131 let fd: i64 = sys_openat_wr(path, 0x1a4) // TRUNCATES the live file NOW 132 if fd < 0 { return 0 - 1 } 133 var off: i64 = 0 134 while off < n { 135 let wr: i64 = sys_write(fd, (bytes as i64 + off) as *u8, n - off) 136 if wr <= 0 { sys_close(fd); return 0 - 2 } 137 off = off + wr 138 } 139 sys_close(fd) 140 return 0 141}