code wiki / (root) / nx_lease_lib.nx

nx_lease_lib.nx source

↩ module page · 492 lines · 25315 B

1// nx_lease_lib.nx -- THE SOVEREIGN LEASE PRIMITIVE as a LIBRARY (extracted VERBATIM from the NAS nx_lease.nx, 2// sha c9eaf727, 2026-09-03, so organs can COMPOSE it instead of forking nx_lease.elf per acquire). Semantics unchanged: 3// first claim by O_CREAT|O_EXCL after an atomic mkdir; takeover of a stale/released lease arbitrated by mkdir on a 4// MONOTONIC GENERATION (stamp field 5 -- never a nonce, see the reverted 2026-08-08 wedge in ls_claim); RELEASE flips 5// the stamp to released and NEVER unlinks (lock dirs are additive). Stamp = one line: 6// held|released<TAB>owner<TAB>epoch<TAB>ttl<TAB>generation 7// NEW here: *_root variants so a caller pins ONE lease root regardless of its CWD, and ls_holder_of for pools that 8// reclaim a dead holder by the OWNER it wrote. nx_lease.nx = this + its CLI main (incl. the forked-racer tooth 8). 9// license_tier: ORIGINAL No hw writes (Rule 26). 10import "nx_syscalls.nx" 11import "nx_srcfresh.nx" // sf_mtime_ns: the age of an orphaned generation gate (2026-09-06, the wedge remedy) 12const LS_MAGIC_3600: i64 = 3600 13 14const LS_STDERR: i64 = 2 15const LS_PATHCAP: i64 = 256 16const LS_STAMPCAP: i64 = 512 17const LS_MODE: i64 = 0x1a4 18const LS_DMODE: i64 = 0x1ed 19const LS_MKDIRAT: i64 = 258 20const LS_ATFDCWD: i64 = 0 - 100 21const LS_GETPID: i64 = 172 22const LS_TAB: i64 = 9 23const LS_NL: i64 = 10 24const LS_D0: i64 = 48 25const LS_D9: i64 = 57 26const LS_B10: i64 = 10 27const LS_NUMB: i64 = 24 28const LS_EXIT_USAGE: i64 = 2 29const LS_EXIT_BUSY: i64 = 3 30const LS_EXIT_IO: i64 = 4 31const LS_EXIT_SELF: i64 = 5 32const LS_V_A: i64 = 97 33const LS_V_R: i64 = 114 34const LS_V_C: i64 = 99 35const LS_V_S: i64 = 115 36const LS_NONCE_MOD: i64 = 1000000 37 38func ls_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 39func ls_werr(s: *u8) -> i64 { sys_write(LS_STDERR, s, ls_slen(s)); return 0 } 40func ls_wn(v: i64) -> i64 { 41 if v == 0 { sys_write(LS_STDERR, "0" as *u8, 1); return 0 } 42 var m: i64 = v 43 if m < 0 { sys_write(LS_STDERR, "-" as *u8, 1); m = 0 - m } 44 let d: *u8 = sys_mmap(LS_NUMB) 45 var k: i64 = 0 46 while m > 0 { d[k] = (LS_D0 + (m % LS_B10)) as u8; m = m / LS_B10; k = k + 1 } 47 let o: *u8 = sys_mmap(LS_NUMB) 48 var i: i64 = 0 49 while i < k { o[i] = d[k - 1 - i]; i = i + 1 } 50 sys_write(LS_STDERR, o, k) 51 return 0 52} 53func ls_cat(d: *u8, off: i64, s: *u8) -> i64 { 54 var o: i64 = off 55 var j: i64 = 0 56 while s[j] != (0 as u8) { d[o] = s[j]; o = o + 1; j = j + 1 } 57 return o 58} 59func ls_catn(d: *u8, off: i64, v: i64) -> i64 { 60 var o: i64 = off 61 var m: i64 = v 62 if m == 0 { d[o] = LS_D0 as u8; return o + 1 } 63 if m < 0 { m = 0 - m } 64 let t: *u8 = sys_mmap(LS_NUMB) 65 var k: i64 = 0 66 while m > 0 { t[k] = (LS_D0 + (m % LS_B10)) as u8; m = m / LS_B10; k = k + 1 } 67 var i: i64 = 0 68 while i < k { d[o] = t[k - 1 - i]; o = o + 1; i = i + 1 } 69 return o 70} 71func ls_eq(a: *u8, b: *u8) -> i64 { 72 var i: i64 = 0 73 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } 74 if b[i] != (0 as u8) { return 0 } 75 return 1 76} 77// name grammar: [a-zA-Z0-9_-]+ only 78func ls_name_ok(s: *u8) -> i64 { 79 var i: i64 = 0 80 if s[0] == (0 as u8) { return 0 } 81 while s[i] != (0 as u8) { 82 let c: i64 = s[i] 83 var ok: i64 = 0 84 if c >= 97 { if c <= 122 { ok = 1 } } 85 if c >= 65 { if c <= 90 { ok = 1 } } 86 if c >= LS_D0 { if c <= LS_D9 { ok = 1 } } 87 if c == 95 { ok = 1 } 88 if c == 45 { ok = 1 } 89 if ok == 0 { return 0 } 90 i = i + 1 91 } 92 return 1 93} 94// ★★THE MUTEX PRIMITIVE. ls_claim USED TO write a temp stamp and sys_renameat() it over `stamp`, 95// then "CAS-verify" by reading the stamp back and checking its own nonce was installed. rename(2) is 96// atomic but LAST-WRITER-WINS -- it does not refuse an existing target -- so every racer that reached 97// ls_claim overwrote the winner and then read back ITS OWN WRITE and confirmed itself the holder. 98// ★★★★★★A COMPARE-AND-SWAP THAT WRITES FIRST AND COMPARES AFTERWARDS IS NOT A CAS, IT IS A WRITE WITH A 99// SELF-CONFIRMING RECEIPT -- IT CANNOT RETURN FALSE, SO IT PROVED MUTUAL EXCLUSION TO EVERY CALLER AT ONCE. 100// O_CREAT|O_EXCL moves the arbitration into the kernel: exactly one creator succeeds, everyone else gets 101// EEXIST. That is a decision the caller cannot fake by writing harder. 102const LS_OPENAT: i64 = 257 103const LS_O_EXCL_WR: i64 = 0xc1 104func ls_open_excl(p: *u8) -> i64 { return __syscall(LS_OPENAT, LS_ATFDCWD, p as i64, LS_O_EXCL_WR, LS_MODE, 0, 0) } 105// ⚠NO ls_unlink, DELIBERATELY AND FOR THE SECOND TIME. This design removes nothing: arbitration is 106// mkdir on a generation, and the old stamp is replaced by rename, never deleted. A dead unlink wrapper 107// beside a mutex is an invitation to reintroduce the ABA, and nx_unwired would flag it regardless. 108func ls_mkdir(p: *u8) -> i64 { return __syscall(LS_MKDIRAT, LS_ATFDCWD, p as i64, LS_DMODE, 0, 0, 0) } 109// ROOT-PARAMETERISED (2026-09-03, for the heavy-I/O slot pool): the lease dir is CWD-relative, and two CWDs 110// (nishihost root vs buildroot) would otherwise make TWO pools -- no bound at all. root ends with '/'. 111func ls_dirpath_root(root: *u8, name: *u8, out: *u8) -> i64 { 112 var o: i64 = ls_cat(out, 0, root) 113 o = ls_cat(out, o, name) 114 o = ls_cat(out, o, ".lock" as *u8) 115 out[o] = 0 as u8 116 return o 117} 118const LS_DEFAULT_ROOT: *u8 = "knowledge/lease/" 119func ls_dirpath(name: *u8, out: *u8) -> i64 { return ls_dirpath_root(LS_DEFAULT_ROOT, name, out) } 120func ls_read(path: *u8, buf: *u8, cap: i64) -> i64 { 121 let fd: i64 = sys_openat_rd(path) 122 if fd < 0 { return 0 - 1 } 123 var n: i64 = 0 124 var go: i64 = 1 125 while go == 1 { 126 let base: i64 = buf as i64 127 let r: i64 = sys_read(fd, (base + n) as *u8, cap - n) 128 if r <= 0 { go = 0 } else { n = n + r } 129 if n >= cap { go = 0 } 130 } 131 sys_close(fd) 132 if n < cap { buf[n] = 0 as u8 } 133 return n 134} 135static g_st: i64 136static g_epoch: i64 137static g_ttl: i64 138static g_nonce: i64 139// field walker: fills ob with field1 (owner), returns numeric fields via statics 140func ls_parse2(buf: *u8, n: i64, ob: *u8, obcap: i64) -> i64 { 141 g_st = 0 142 g_epoch = 0 143 g_ttl = 0 144 g_nonce = 0 145 if n <= 0 { return 0 } 146 if buf[0] == (104 as u8) { g_st = 1 } 147 if buf[0] == (114 as u8) { g_st = 2 } 148 if g_st == 0 { return 0 } 149 var i: i64 = 0 150 var field: i64 = 0 151 var ok2: i64 = 0 152 var v: i64 = 0 153 while i < n { 154 let c: i64 = buf[i] 155 if c == LS_TAB { 156 if field == 2 { g_epoch = v } 157 if field == 3 { g_ttl = v } 158 field = field + 1 159 v = 0 160 if field == 1 { ok2 = 0 } 161 } else { 162 if field == 1 { if ok2 < obcap - 1 { ob[ok2] = c as u8; ok2 = ok2 + 1; ob[ok2] = 0 as u8 } } 163 if c >= LS_D0 { if c <= LS_D9 { v = v * LS_B10 + (c - LS_D0) } } 164 if c == LS_NL { i = n } 165 } 166 i = i + 1 167 } 168 g_nonce = v 169 return 1 170} 171// gate < 0 -> FIRST claim: no stamp is being replaced, so an exclusive create of `stamp` IS the 172// arbitration (initial claim after winning mkdir, and the stampless/crashed-mid-acquire 173// case, where there is nothing to remove). 174// gate >= 0 -> TAKEOVER: we READ a stamp and judged it released/expired, and `gate` is THAT STAMP'S 175// NONCE. We must win `claim.<gate>` exclusively before replacing it. 176// ★★★★★★NOTHING IS EVER UNLINKED HERE, WHICH IS WHY THE ABA IS GONE RATHER THAN NARROWED. The previous 177// version unlinked the old stamp and then created a new one, so taker B's unlink could delete taker A's 178// FRESH stamp and both would "win". Gating on a file NAMED FOR THE NONCE BEING REPLACED makes the race 179// self-limiting: every taker that read stamp N competes for claim.N and exactly one wins; a taker that 180// arrives later reads the NEW stamp and competes for a different, uncontended name. The identity of what 181// we are replacing is carried IN THE LOCK NAME, so a stale reader cannot collide with a fresh one. 182// ★★★★★DERIVE THE LOCK NAME FROM THE STATE YOU INTEND TO REPLACE, AND ABA CANNOT BE EXPRESSED. 183// The tmp+renameat below is safe HERE precisely because it runs only after that exclusive win -- the 184// same primitive that was the original defect is correct once exactly one process can reach it. 185// ⚠Rule 13: claim.<n> files are additive and never reaped; they are ~0 bytes and bounded by takeovers. 186// ⚠THE LAST STAMP FIELD IS A GENERATION, NOT A NONCE. It was called a nonce and was never one 187// (pid*1e6+sec%1e6 repeats within a process-second). It is now monotonic per lease: the taker that wins 188// g<G+1> writes G+1. Renamed at the parameter so the next reader cannot mistake it for entropy. 189func ls_claim(dir: *u8, owner: *u8, ttl: i64, gen: i64, gate: i64) -> i64 { 190 let sp: *u8 = sys_mmap(LS_PATHCAP) 191 var o: i64 = ls_cat(sp, 0, dir) 192 o = ls_cat(sp, o, "/stamp" as *u8) 193 sp[o] = 0 as u8 194 let sb: *u8 = sys_mmap(LS_STAMPCAP) 195 var so: i64 = ls_cat(sb, 0, "held" as *u8) 196 sb[so] = LS_TAB as u8 197 so = so + 1 198 so = ls_cat(sb, so, owner) 199 sb[so] = LS_TAB as u8 200 so = so + 1 201 so = ls_catn(sb, so, sys_now_realtime_sec()) 202 sb[so] = LS_TAB as u8 203 so = so + 1 204 so = ls_catn(sb, so, ttl) 205 sb[so] = LS_TAB as u8 206 so = so + 1 207 so = ls_catn(sb, so, gen) 208 sb[so] = LS_NL as u8 209 so = so + 1 210 // A create/open that does not clearly succeed collapses to 0 = LOST, which every caller already 211 // reads as busy. EEXIST and a real io error are deliberately NOT distinguished: a lease that cannot 212 // prove it won must report that it lost. ★FAIL TOWARDS NOT HOLDING THE MUTEX. 213 if gate < 0 { 214 let fd: i64 = ls_open_excl(sp) 215 if fd < 0 { return 0 } 216 sys_write(fd, sb, so) 217 sys_close(fd) 218 return 1 219 } 220 // 🔴REVERTED 2026-08-08, SAME SESSION, AFTER MEASURING THE REPLACEMENT. I shipped a claim.<nonce> 221 // gate here to close the takeover ABA by construction. IT WEDGED THE LEASE PERMANENTLY, because the 222 // "nonce" it keyed on is not unique: ls_nonce is pid*1e6 + (seconds % 1e6), which is THE SAME VALUE 223 // for every call a process makes within one second. A process that took over and then RELEASED in 224 // the same second wrote a released stamp bearing the number of the claim file it had just created, 225 // so the next taker gated on a name that already existed and every later acquire returned BUSY -- 226 // FOREVER, SILENTLY. Measured live: 7 of 9 selftest teeth failed and `acquire lease_probe` answered 227 // LS-BUSY against a RELEASED stamp. 228 // ★★★★★★A VALUE NAMED "NONCE" IS NOT A NONCE. THIS ONE HAS BEEN COLLIDING SINCE THE DAY IT WAS 229 // WRITTEN; IT WAS HARMLESS ONLY BECAUSE THE OLD FAKE-CAS NEVER ACTUALLY COMPARED IT. 230 // ★★★★★★A LOCK THAT CAN WEDGE FOREVER IS STRICTLY WORSE THAN ONE THAT OCCASIONALLY ADMITS TWO: THE 231 // SECOND FAILS TOWARD PROGRESS AND THE CALLER'S OWN IDEMPOTENCE ABSORBS IT, THE FIRST STOPS THE 232 // ESTATE AND CANNOT RECOVER WITHOUT A HUMAN. WHEN TRADING ONE DEFECT FOR ANOTHER, COMPARE THE 233 // FAILURE MODES, NOT THE ELEGANCE. 234 // So this is back to the PROVEN behaviour (tooth 8: 6/6 GREEN, 120 acquisitions, one winner each). 235 // The residual takeover ABA stays open on debt 1786244397 -- whose real prerequisite is now known to 236 // be A GENUINELY UNIQUE STAMP IDENTITY, not the gating scheme I tried. 237 // TAKEOVER, ARBITRATED BY mkdir ON A MONOTONIC GENERATION. 238 // `gate` is the generation of the stamp we READ and judged dead. mkdir(2) is an atomic test-and-set 239 // on a NAME, so of every taker that read generation G, exactly one creates g<G+1> and the rest get 240 // EEXIST and correctly report busy. The winner then installs the new stamp carrying G+1. 241 // ★★★★★★WHY THIS IS NOT MY EARLIER WEDGE: that version keyed the same idea on ls_nonce, which is 242 // pid*1e6+(sec%1e6) and therefore CONSTANT within a process-second -- a take-then-release in one 243 // second wrote a stamp bearing the claim file's own name and the lease jammed forever. A GENERATION 244 // CANNOT REPEAT, BECAUSE THE WINNER IS THE ONE WHO INCREMENTS IT. Same shape, sound key. 245 // ★WHY THE STAMPLESS PATH CANNOT COLLIDE WITH A STALE g<N>: a g<N> dir exists only if a takeover 246 // happened, and a takeover requires a stamp to have been read. The stampless case is the crash window 247 // between mkdir and the FIRST stamp write, where no takeover can yet have occurred -- so gen 1 is 248 // always free there. The reset-after-crash hazard I worried about is unreachable by construction. 249 return ls_claim_takeover(dir, sp, sb, so, gen, gate, ls_orphan_age_s(ttl)) 250} 251// THE TAKEOVER, WITH CRASH RECOVERY (2026-09-06). Before this: mkdir g<gate> FIRST, then write a temp stamp, then 252// rename it over `stamp`. A process killed (or a write refused) between the mkdir and the rename left g<gate> on 253// disk with the stamp still reading gate-1, and every later takeover re-derived the same gate, hit EEXIST and 254// reported BUSY -- forever. MEASURED: all four knowledge/lease/heavyio-slot-N.lock slots wedged on 2026-09-04 (the 255// crawler and shard-compactor dead 18 h unnoticed) and AGAIN on 2026-09-06 (stamps released at gens 236/96/163/157, 256// g237/g97/g164/g158 present), which is why nx_swcompare_evidence deferred every pass of the day with slot=-1 while 257// running=1 of width=4. nx_lease_wedge_gate detects the shape; this is the remedy, in two bounded parts: 258// (1) the temp stamp is written FIRST, under a name private to this claimant (gen AND pid), so a refused write can 259// no longer strand a gate, and two racers at one gate never share a temp file; 260// (2) on EEXIST the gate's AGE decides: a gate younger than orphan_age_s belongs to a live claim in flight (a claim 261// spans milliseconds) and the takeover is LOST as before; a gate at least orphan_age_s old with the stamp still 262// one generation behind it is the orphan of a dead takeover and is removed and retaken. The age floor is 263// max(ttl, LS_ORPHAN_MIN_S): the same imprecision the pool already accepts for a hung-but-alive holder. 264// Nothing here unlinks a STAMP -- the ABA named above stays closed; only an EMPTY gate directory that no stamp ever 265// advanced to is removed, and only after it has outlived any claim that could still be writing. The generation stays 266// monotonic: the reclaimer re-creates the SAME gate and installs the SAME generation the dead taker would have. 267const LS_ORPHAN_MIN_S: i64 = 30 268const LS_UNLINKAT: i64 = 263 269const LS_AT_REMOVEDIR: i64 = 512 270func ls_rmdir(p: *u8) -> i64 { return __syscall(LS_UNLINKAT, LS_ATFDCWD, p as i64, LS_AT_REMOVEDIR, 0, 0, 0) } 271func ls_orphan_age_s(ttl: i64) -> i64 { if ttl > LS_ORPHAN_MIN_S { return ttl } return LS_ORPHAN_MIN_S } 272// age of a path in whole seconds by mtime; -1 when it cannot be stat'd (an absent gate is not an orphan) 273func ls_path_age_s(p: *u8) -> i64 { 274 let m: i64 = sf_mtime_ns(p) 275 if m < 0 { return 0 - 1 } 276 let age: i64 = sys_now_realtime_sec() - m / SF_NS_PER_SEC 277 if age < 0 { return 0 } 278 return age 279} 280func ls_claim_takeover(dir: *u8, sp: *u8, sb: *u8, so: i64, gen: i64, gate: i64, orphan_age_s: i64) -> i64 { 281 let tp: *u8 = sys_mmap(LS_PATHCAP) 282 var to: i64 = ls_cat(tp, 0, dir) 283 to = ls_cat(tp, to, "/stamp.t" as *u8) 284 to = ls_catn(tp, to, gen) 285 to = ls_cat(tp, to, "." as *u8) 286 to = ls_catn(tp, to, ls_pid()) 287 tp[to] = 0 as u8 288 let tfd: i64 = sys_openat_wr(tp, LS_MODE) 289 if tfd < 0 { return 0 } 290 sys_write(tfd, sb, so) 291 sys_close(tfd) 292 let gp: *u8 = sys_mmap(LS_PATHCAP) 293 var go2: i64 = ls_cat(gp, 0, dir) 294 go2 = ls_cat(gp, go2, "/g" as *u8) 295 go2 = ls_catn(gp, go2, gate) 296 gp[go2] = 0 as u8 297 var mk: i64 = ls_mkdir(gp) 298 if mk != 0 { 299 let age: i64 = ls_path_age_s(gp) 300 if age >= 0 { if age >= orphan_age_s { 301 ls_werr("LS-ORPHAN-GATE reclaimed: " as *u8); ls_werr(gp); ls_werr(" (a takeover died between its mkdir and its rename; the stamp never advanced to this generation)\n" as *u8) 302 if ls_rmdir(gp) == 0 { mk = ls_mkdir(gp) } 303 } } 304 if mk != 0 { sys_unlinkat(tp); return 0 } 305 } 306 // Sole winner for this generation: install via rename. No other process can reach here. 307 if sys_renameat(tp, sp) < 0 { return 0 } 308 return 1 309} 310// the same takeover with the orphan age chosen by the caller -- for gates that plant an orphan and must not wait 311func ls_claim_x(dir: *u8, owner: *u8, ttl: i64, gen: i64, gate: i64, orphan_age_s: i64) -> i64 { 312 let sp: *u8 = sys_mmap(LS_PATHCAP) 313 var o: i64 = ls_cat(sp, 0, dir) 314 o = ls_cat(sp, o, "/stamp" as *u8) 315 sp[o] = 0 as u8 316 let sb: *u8 = sys_mmap(LS_STAMPCAP) 317 var so: i64 = ls_cat(sb, 0, "held" as *u8) 318 sb[so] = LS_TAB as u8 319 so = so + 1 320 so = ls_cat(sb, so, owner) 321 sb[so] = LS_TAB as u8 322 so = so + 1 323 so = ls_catn(sb, so, sys_now_realtime_sec()) 324 sb[so] = LS_TAB as u8 325 so = so + 1 326 so = ls_catn(sb, so, ttl) 327 sb[so] = LS_TAB as u8 328 so = so + 1 329 so = ls_catn(sb, so, gen) 330 sb[so] = LS_NL as u8 331 so = so + 1 332 return ls_claim_takeover(dir, sp, sb, so, gen, gate, orphan_age_s) 333} 334func ls_stamp_path(dir: *u8, out: *u8) -> i64 { 335 var o: i64 = ls_cat(out, 0, dir) 336 o = ls_cat(out, o, "/stamp" as *u8) 337 out[o] = 0 as u8 338 return o 339} 340// release = install a released-stamp (never unlink/rmdir) 341func ls_unclaim(dir: *u8, owner: *u8, nonce: i64) -> i64 { 342 let sp: *u8 = sys_mmap(LS_PATHCAP) 343 ls_stamp_path(dir, sp) 344 let tp: *u8 = sys_mmap(LS_PATHCAP) 345 var o2: i64 = ls_cat(tp, 0, dir) 346 o2 = ls_cat(tp, o2, "/stamp.r" as *u8) 347 o2 = ls_catn(tp, o2, nonce) 348 tp[o2] = 0 as u8 349 let sb: *u8 = sys_mmap(LS_STAMPCAP) 350 var so: i64 = ls_cat(sb, 0, "released" as *u8) 351 sb[so] = LS_TAB as u8 352 so = so + 1 353 so = ls_cat(sb, so, owner) 354 sb[so] = LS_TAB as u8 355 so = so + 1 356 so = ls_catn(sb, so, sys_now_realtime_sec()) 357 sb[so] = LS_TAB as u8 358 so = so + 1 359 so = ls_catn(sb, so, 0) 360 sb[so] = LS_TAB as u8 361 so = so + 1 362 so = ls_catn(sb, so, nonce) 363 sb[so] = LS_NL as u8 364 so = so + 1 365 let fd: i64 = sys_openat_wr(tp, LS_MODE) 366 if fd < 0 { return 0 - 1 } 367 sys_write(fd, sb, so) 368 sys_close(fd) 369 if sys_renameat(tp, sp) < 0 { return 0 - 1 } 370 return 1 371} 372func ls_pid() -> i64 { return __syscall(LS_GETPID, 0, 0, 0, 0, 0, 0) } 373// core acquire, returns 0 acquired / 1 busy / -1 io ; on busy the holder is in hb 374func ls_acquire_root(root: *u8, name: *u8, owner: *u8, ttl: i64, hb: *u8) -> i64 { 375 ls_mkdir(root) 376 let dir: *u8 = sys_mmap(LS_PATHCAP) 377 ls_dirpath_root(root, name, dir) 378 let nonce: i64 = ls_pid() * LS_NONCE_MOD + (sys_now_realtime_sec() % LS_NONCE_MOD) 379 let mk: i64 = ls_mkdir(dir) 380 if mk == 0 { 381 // FIRST claim into a dir we just won with mkdir: generation 1, no gate needed (nothing to replace). 382 let c: i64 = ls_claim(dir, owner, ttl, 1, 0 - 1) 383 if c == 1 { return 0 } 384 return 0 - 1 385 } 386 // dir exists: read stamp 387 let sp: *u8 = sys_mmap(LS_PATHCAP) 388 ls_stamp_path(dir, sp) 389 let rb: *u8 = sys_mmap(LS_STAMPCAP) 390 var rn: i64 = ls_read(sp, rb, LS_STAMPCAP - 1) 391 if rn <= 0 { 392 // ★A STAMPLESS DIR MEANS TWO OPPOSITE THINGS: "acquire IN FLIGHT, microseconds old" and 393 // "acquirer CRASHED, abandoned". The original code assumed CRASHED -- the unsafe reading -- 394 // which is why forked racers could all acquire one lease (MEASURED by selftest tooth 8: 395 // acquirers=2, 3 and 4 across rounds; 3 of 5 rounds failed). 396 // The mkdir above is already an ATOMIC mutex and its winner writes the stamp almost at once, 397 // so RE-READ a bounded number of times before concluding the holder died. Each ls_read is a 398 // syscall, which yields the winner a scheduling opportunity. 399 // ✅SUPERSEDED AS A CORRECTNESS MEASURE -- the O_CREAT|O_EXCL claim named here HAS SINCE LANDED 400 // (see ls_claim), so exclusion no longer depends on this loop at all and a loser that reads a 401 // stampless dir now simply fails its exclusive create. THIS RETRY IS RETAINED FOR A DIFFERENT 402 // REASON, stated so nobody deletes it as dead weight or trusts it as the mutex: ls_acquire 403 // returns the HOLDER in hb on busy, and a racer that gives up before the stamp appears reports 404 // "busy, holder unknown". The re-read is what makes the busy answer NAME who holds it. 405 // ★★★★★A COMMENT THAT STILL DESCRIBES THE FIX AS PENDING AFTER IT HAS SHIPPED WILL BE READ AS THE 406 // CURRENT STATE OF THE CODE -- STALE RATIONALE IN A MUTEX IS ITSELF A DEFECT. 407 // Tooth 8 remains the acceptance test and must be re-run several times: the defect it hunts 408 // only fired in ~60-80% of rounds, so a single green proves nothing. 409 let retry_cap: i64 = 64 // syscall-paced retries, named rather than magic (rule 11) 410 var stries: i64 = 0 411 while stries < retry_cap { 412 rn = ls_read(sp, rb, LS_STAMPCAP - 1) 413 if rn > 0 { stries = retry_cap } 414 if rn <= 0 { stries = stries + 1 } 415 } 416 } 417 if rn <= 0 { 418 // dir with no stamp (crashed mid-acquire) = claimable 419 // ⚠takeover=0, NOT 1. "dir exists but has no stamp" means there is NO FILE TO REMOVE, so an 420 // unlink here can only delete a stamp that appeared BETWEEN our read and our write -- i.e. the 421 // winner's. Routing this path through takeover made all 4 racers cascade: A creates, B unlinks 422 // A's fresh stamp, B creates, C unlinks B's ... everyone "wins". ★★★★★★I NAMED THIS ABA WINDOW IN 423 // THE COMMENT ABOVE ls_claim AND THEN ROUTED THE MOST COMMON PATH STRAIGHT THROUGH IT -- WRITING 424 // DOWN A HAZARD IS NOT THE SAME ACT AS KEEPING CODE OUT OF IT. 425 // With takeover=0 the loser's O_EXCL create simply fails against the winner's stamp and it 426 // correctly reports busy. Only c3/c4 -- where we have actually READ a released/expired stamp -- 427 // have something real to replace. 428 let c2: i64 = ls_claim(dir, owner, ttl, 1, 0 - 1) 429 if c2 == 1 { return 0 } 430 if c2 == 0 { return 1 } 431 return 0 - 1 432 } 433 if ls_parse2(rb, rn, hb, LS_PATHCAP) == 0 { return 0 - 1 } 434 if g_st == 2 { 435 // g_nonce is the nonce of the RELEASED stamp we just parsed -- gate the takeover on it. 436 let c3: i64 = ls_claim(dir, owner, ttl, g_nonce + 1, g_nonce + 1) 437 if c3 == 1 { return 0 } 438 if c3 == 0 { return 1 } 439 return 0 - 1 440 } 441 // lease validity = [epoch, epoch+ttl) -- expiry is INCLUSIVE (ttl=0 => reclaimable at once) 442 let now: i64 = sys_now_realtime_sec() 443 if now >= g_epoch + g_ttl { 444 // g_nonce is the nonce of the EXPIRED stamp we just parsed -- gate the takeover on it. 445 let c4: i64 = ls_claim(dir, owner, ttl, g_nonce + 1, g_nonce + 1) 446 if c4 == 1 { return 0 } 447 if c4 == 0 { return 1 } 448 return 0 - 1 449 } 450 return 1 451} 452func ls_release_root(root: *u8, name: *u8, owner: *u8) -> i64 { 453 let dir: *u8 = sys_mmap(LS_PATHCAP) 454 ls_dirpath_root(root, name, dir) 455 let sp: *u8 = sys_mmap(LS_PATHCAP) 456 ls_stamp_path(dir, sp) 457 let rb: *u8 = sys_mmap(LS_STAMPCAP) 458 let rn: i64 = ls_read(sp, rb, LS_STAMPCAP - 1) 459 if rn <= 0 { return 1 } 460 let ob: *u8 = sys_mmap(LS_PATHCAP) 461 if ls_parse2(rb, rn, ob, LS_PATHCAP) == 0 { return 0 - 1 } 462 if g_st != 1 { return 1 } 463 if ls_eq(ob, owner) == 0 { return 1 } 464 // ★★★★★★RELEASE MUST PRESERVE THE GENERATION, NOT MINT A NEW VALUE. g_nonce here is the generation 465 // parsed from the stamp we are releasing. Writing a fresh pid/clock value would break monotonicity 466 // and could land on a generation whose g<N> dir ALREADY EXISTS -- the next taker would then fail its 467 // mkdir gate forever and the lease would wedge. That is the precise failure this design replaced; 468 // re-introducing it here would be invisible until a release happened to collide. 469 if ls_unclaim(dir, owner, g_nonce) == 1 { return 0 } 470 return 0 - 1 471} 472 473// The ORIGINAL entry points, behaviour-identical: default root, plus the knowledge/ mkdir the old body did (rule 20). 474func ls_acquire(name: *u8, owner: *u8, ttl: i64, hb: *u8) -> i64 { 475 ls_mkdir("knowledge" as *u8) 476 return ls_acquire_root(LS_DEFAULT_ROOT, name, owner, ttl, hb) 477} 478func ls_release(name: *u8, owner: *u8) -> i64 { return ls_release_root(LS_DEFAULT_ROOT, name, owner) } 479// Holder introspection for a POOL: fills hb with the holder OWNER and returns the stamp state -- 1 held, 2 released, 480// 0 absent/stampless, -1 io. The pool derives liveness from the OWNER string it wrote itself (pid<n>); stamp field 5 481// is a GENERATION here, never a pid, so nothing is read from it. 482func ls_holder_of(root: *u8, name: *u8, hb: *u8) -> i64 { 483 let dir: *u8 = sys_mmap(LS_PATHCAP) 484 ls_dirpath_root(root, name, dir) 485 let sp: *u8 = sys_mmap(LS_PATHCAP) 486 ls_stamp_path(dir, sp) 487 let rb: *u8 = sys_mmap(LS_STAMPCAP) 488 let rn: i64 = ls_read(sp, rb, LS_STAMPCAP - 1) 489 if rn <= 0 { return 0 } 490 if ls_parse2(rb, rn, hb, LS_PATHCAP) == 0 { return 0 - 1 } 491 return g_st 492}