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}