code wiki / _hdl_build / nx_ws_cas.nx
nx_ws_cas.nx source
↩ module page · 55 lines · 3195 B
1// nx_ws_cas.nx -- WMS-R8: CONCURRENT-SSOT-SAFE registry write (advisory-lock serialized).
2//
3// module: nishi-core.wms.ws_cas
4// capability: CORE_COMPUTE (the registry's concurrent-writer durability floor)
5//
6// ROOT CAUSE this closes: ws_put_p (nx_workstream_store) is an UNGUARDED read-modify-write --
7// ss_get (idempotency probe) -> ss_begin/ss_add -> ws_seg_next (= 1 + ss_manifest line count)
8// -> ss_commit (writes the seg files, then RMW-appends "seg-<id>" to manifest.txt via tmp+rename).
9// TWO concurrent ws_put_p calls race on BOTH the segid AND manifest.txt: each reads the SAME
10// manifest line count -> computes the SAME segid -> ss_write_seg clobbers the other's segment; and
11// each reads the SAME old manifest.txt, appends ITS one line, renames -> last-writer-wins, so all
12// but one concurrent manifest append is LOST. Net: committed records become UNRETRIEVABLE (the
13// classic lost update) even though each ss_commit's own tmp+rename is atomic. ss_commit gives
14// crash-atomicity, NOT inter-writer mutual exclusion -- the registry RMW needs a lock the substrate
15// never applied to it (the audit said the locking primitive was ABSENT; R0b added sys_flock).
16//
17// THE FIX (one capability): serialize the WHOLE ws_put_p RMW under an EXCLUSIVE advisory lock
18// (flock LOCK_EX) on a per-prefix lock file <prefix>wlock -- mirroring the WMS-R0b framed-append
19// lock frame. Two writers can no longer both be inside the manifest RMW window -> every commit gets
20// a distinct increasing segid and a non-clobbered manifest line -> ZERO lost updates.
21//
22// REUSE (rule 15): the lock frame is the proven nx_framed_append pattern (sys_flock LOCK_EX/UN from
23// nx_syscalls); the write itself REUSES ws_put_p UNCHANGED (additive -- the unlocked path stays for
24// genuine single-writer callers). Sovereign: nx_workstream_store + nx_syscalls only.
25// license_tier: ORIGINAL
26import "nx_workstream_store.nx"
27import "nx_syscalls.nx"
28
29// derive the per-prefix exclusive write-lock path: <prefix> + "wlock" (NUL-term) into out.
30// (a sibling of the store's manifest.txt; ONE lock guards the whole prefix's RMW.)
31func wc_lockpath(prefix: *u8, out: *u8) -> i64 {
32 var i: i64 = 0
33 while prefix[i] != (0 as u8) { out[i] = prefix[i]; i = i + 1 }
34 let suf: *u8 = "wlock" as *u8
35 var j: i64 = 0
36 while suf[j] != (0 as u8) { out[i] = suf[j]; i = i + 1; j = j + 1 }
37 out[i] = 0 as u8
38 return i
39}
40
41// ws_put_locked: ws_put_p serialized under flock(LOCK_EX) on <prefix>wlock so concurrent writers
42// can never collide on segid/manifest. Returns ws_put_p's verdict (0 committed / 1 unchanged /
43// <0 commit error), or -10 if the lock file cannot be opened. A flock error is NON-fatal (some
44// filesystems lack locks) -- it is best-effort and ws_put_p still runs (O_APPEND/tmp+rename remain).
45func ws_put_locked(prefix: *u8, key: *u8, val: *u8) -> i64 {
46 let lp: *u8 = sys_mmap(512)
47 wc_lockpath(prefix, lp)
48 let lfd: i64 = sys_openat_append(lp, 0x1a4) // O_WRONLY|O_CREAT|O_APPEND, mode 0644
49 if lfd < 0 { return 0 - 10 }
50 sys_flock(lfd, SYS_LOCK_EX)
51 let rc: i64 = ws_put_p(prefix, key, val)
52 sys_flock(lfd, SYS_LOCK_UN)
53 sys_close(lfd)
54 return rc
55}