code wiki / _hdl_build / nx_dispatch_lease.nx

nx_dispatch_lease.nx source

↩ module page · 273 lines · 12253 B

1// nx_dispatch_lease.nx -- WMS rung M4: the LEASING DISPATCHER / SCHEDULER. 2// 3// module: nishi-core.autonomy.dispatch_lease 4// capability: DISPATCH_LEASE_NEXT_READY_RUNG (frontier-aware pick + O_EXCL lease) 5// 6// WHAT THIS ADDS (the missing capability): nx_assign_next/an_pick are READ-ONLY -- 7// they NAME the next runnable rung but never CLAIM it, so two parallel workers both 8// see the same "next" and collide on one stream. M4 LEASES the pick under an 9// O_CREAT|O_EXCL lockfile (the kernel guarantees exactly one creator wins -- no 10// TOCTOU), so exactly one owner holds a given stream at a time. Generalizes the 11// conductor's single-arc drill into "keep the WHOLE machine moving": if the top 12// pick is already leased, the dispatcher scans down the w-ordering and leases the 13// NEXT ready stream instead of stalling. 14// 15// REUSE / lineage (compose, do not reinvent): 16// - an_load / an_pick / an_deps_done / an_id_at / an_find <- nx_assign_core (the 17// dep-graph + readiness + frontier-aware pick are ALREADY correct there) 18// - O_EXCL acquire/steal lock <- the rt_lock idiom from nx_registry_lock (INLINED 19// here as dl_lock/dl_unlock so this file keeps a SINGLE nx_syscalls surface and 20// dodges the double-import rc=6 trap -- nx_assign_core already pulls nx_syscalls 21// transitively, importing nx_registry_lock too would surface it twice) 22// - fa_cat / fa_catn / fa_appendz <- nx_framed_append (every status write is ONE 23// buffer -> ONE locked atomic fa_appendz; the torn-line bug we must NOT reintroduce) 24// 25// IMPORT DISCIPLINE: import nx_assign_core (surfaces nx_syscalls) + nx_framed_append 26// (also needs nx_syscalls). The prepass dedups nx_syscalls across imports (proven: 27// nx_assign_next imports BOTH nx_syscalls AND nx_assign_core and builds clean). We do 28// NOT import nx_registry_lock (its O_EXCL logic is inlined) -> only two import edges. 29// license_tier: ORIGINAL 30import "nx_assign_core.nx" 31import "nx_framed_append.nx" 32const DL_MAGIC_4096: i64 = 4096 33 34const DL_OEXCL: i64 = 193 // O_CREAT(0x40)|O_EXCL(0x80)|O_WRONLY(0x1) 35const DL_MODE: i64 = 420 // 0644 36const DL_STALE_SEC: i64 = 30 // steal a lease whose stamped epoch is older than this (crashed holder) 37const DL_REC_CAP: i64 = 256 // bounded status record size (no magic number) 38const DL_IDCAP: i64 = 32 // mirrors AN_IDCAP -- max business-key length 39 40// ---- string helpers (sovereign, local; mirror the substrate idioms) ---- 41func dl_streq(a: *u8, b: *u8) -> i64 { 42 var i: i64 = 0 43 while i < 64 { 44 if a[i] != b[i] { return 0 } 45 if a[i] == (0 as u8) { return 1 } 46 i = i + 1 47 } 48 return 1 49} 50 51// build "<dir>/<id>.lease" (NUL-terminated) into out; dir has NO trailing slash. 52// Returns out so callers can pass it straight into dl_lock. 53func dl_lease_path(dir: *u8, id: *u8, out: *u8) -> *u8 { 54 var o: i64 = 0 55 var i: i64 = 0 56 while dir[i] != (0 as u8) { out[o] = dir[i]; o = o + 1; i = i + 1 } 57 out[o] = 47 as u8; o = o + 1 // '/' 58 i = 0 59 while id[i] != (0 as u8) { out[o] = id[i]; o = o + 1; i = i + 1 } 60 out[o] = 46 as u8; o = o + 1 // '.' 61 out[o] = 108 as u8; o = o + 1 // 'l' 62 out[o] = 101 as u8; o = o + 1 // 'e' 63 out[o] = 97 as u8; o = o + 1 // 'a' 64 out[o] = 115 as u8; o = o + 1 // 's' 65 out[o] = 101 as u8; o = o + 1 // 'e' 66 out[o] = 0 as u8 67 return out 68} 69 70// ---- INLINED O_EXCL lease lock (rt_lock idiom; single nx_syscalls surface) ---- 71// stamp the holder's acquire-epoch into the lockfile (staleness channel for contenders) 72func dl_writeint_fd(fd: i64, v: i64) -> i64 { 73 let b: *u8 = sys_mmap(28) 74 var m: i64 = v 75 if m == 0 { b[0] = 48 as u8; sys_write(fd, b, 1); return 0 } 76 let t: *u8 = sys_mmap(28) 77 var k: i64 = 0 78 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 79 var i: i64 = 0 80 while i < k { b[i] = t[k - 1 - i]; i = i + 1 } 81 sys_write(fd, b, k) 82 return 0 83} 84func dl_readint(path: *u8) -> i64 { 85 let fd: i64 = sys_openat_rd(path) 86 if fd < 0 { return 0 } 87 let b: *u8 = sys_mmap(64) 88 let n: i64 = sys_read(fd, b, 63) 89 sys_close(fd) 90 var v: i64 = 0 91 var i: i64 = 0 92 while i < n { if b[i] >= (48 as u8) { if b[i] <= (57 as u8) { v = v * 10 + ((b[i] as i64) - 48) } } i = i + 1 } 93 return v 94} 95func dl_unlink(path: *u8) -> i64 { return __syscall(263, AT_FDCWD, path as i64, 0, 0, 0, 0) } 96 97// ACQUIRE a lease: O_CREAT|O_EXCL open of lockpath; exactly one creator wins. A 98// contender STEALS a lease whose stamped epoch is older than DL_STALE_SEC (a crashed 99// holder cannot wedge the machine). Bounded-spin once (this is a TRY: a held lease 100// returns -1 fast so the dispatcher can move to the next ready stream rather than 101// blocking on one collision). Returns the held fd (>=0) or -1 (already held). 102func dl_lock(lockpath: *u8) -> i64 { 103 let fd: i64 = __syscall(SYS_OPENAT, AT_FDCWD, lockpath as i64, DL_OEXCL, DL_MODE, 0, 0) 104 if fd >= 0 { 105 dl_writeint_fd(fd, sys_now_realtime_sec()) // stamp acquire-epoch 106 return fd 107 } 108 // already exists -- check staleness; steal a dead holder's lease then retry once. 109 let held: i64 = dl_readint(lockpath) 110 if held > 0 { 111 if sys_now_realtime_sec() - held > DL_STALE_SEC { 112 dl_unlink(lockpath) 113 let fd2: i64 = __syscall(SYS_OPENAT, AT_FDCWD, lockpath as i64, DL_OEXCL, DL_MODE, 0, 0) 114 if fd2 >= 0 { dl_writeint_fd(fd2, sys_now_realtime_sec()); return fd2 } 115 } 116 } 117 return 0 - 1 118} 119func dl_unlock(lockpath: *u8, fd: i64) -> i64 { sys_close(fd); dl_unlink(lockpath); return 0 } 120 121// ---- the M4 lease API (composes an_* + the inlined lock) ---- 122 123// Is stream r dispatchable? status==TODO(84) AND every dep DONE. 124func dl_ready(cx: *i64, r: i64) -> i64 { 125 let st: *i64 = cx[4] as *i64 126 if st[r] != 84 { return 0 } // 'T' TODO 127 return an_deps_done(cx, r) 128} 129 130// pick the next READY rung across ALL rows (frontier-aware): an_pick already enforces 131// highest-w + deps-done + tie smaller-size + file-order. Returns row idx or -1. 132func dl_pick_ready(cx: *i64) -> i64 { 133 return an_pick(cx) 134} 135 136// TRY-LEASE one stream by id under <dir>/<id>.lease. Returns the held lockfd (>=0) 137// iff THIS caller won the exclusive lease, or -1 if already held (DENIED). 138func dl_try_lease(dir: *u8, id: *u8) -> i64 { 139 let lp: *u8 = sys_mmap(512) 140 dl_lease_path(dir, id, lp) 141 return dl_lock(lp) 142} 143 144// RELEASE a held lease: close fd + unlink lease file (the stream is re-dispatchable). 145func dl_release(dir: *u8, id: *u8, fd: i64) -> i64 { 146 let lp: *u8 = sys_mmap(512) 147 dl_lease_path(dir, id, lp) 148 return dl_unlock(lp, fd) 149} 150 151// the LEASE-EXISTS probe (read-only): 1 if a lease lockfile is currently present. 152// Lets the keep-moving scan skip an already-leased stream WITHOUT consuming a steal. 153func dl_lease_held(dir: *u8, id: *u8) -> i64 { 154 let lp: *u8 = sys_mmap(512) 155 dl_lease_path(dir, id, lp) 156 let fd: i64 = sys_openat_rd(lp) 157 if fd < 0 { return 0 } 158 sys_close(fd) 159 return 1 160} 161 162// DISPATCH = pick a READY rung and LEASE it atomically. Frontier-aware + keep-moving: 163// 1. r = an_pick (top ready by w); if none ready -> -1. 164// 2. try-lease that id; if won -> return its id in id_out, fd in fd_out. 165// 3. if the top pick is already leased by another worker, scan the REMAINING ready 166// rows in w-descending order and lease the first un-leased one ("keep the whole 167// machine moving" rather than colliding or stalling). 168// Returns the held lockfd (>=0) with id_out NUL-terminated, or -1 if nothing ready / 169// every ready stream is already leased. A BLOCKED stream is NEVER returned (dl_ready 170// gates the scan) -- that is the structural neg-control guarantee. 171func dl_dispatch(cx: *i64, dir: *u8, id_out: *u8, fd_out: *i64) -> i64 { 172 let r: i64 = dl_pick_ready(cx) 173 if r < 0 { return 0 - 1 } 174 let topid: *u8 = an_id_at(cx, r) 175 let fd: i64 = dl_try_lease(dir, topid) 176 if fd >= 0 { 177 var i: i64 = 0 178 while topid[i] != (0 as u8) { id_out[i] = topid[i]; i = i + 1 } 179 id_out[i] = 0 as u8 180 fd_out[0] = fd 181 return fd 182 } 183 // top pick already leased -> keep moving: lease the next-best READY un-leased row. 184 let wv: *i64 = cx[3] as *i64 185 let sz: *i64 = cx[5] as *i64 186 var best: i64 = 0 - 1 187 var guard: i64 = 0 188 var done: i64 = 0 189 while done == 0 { 190 // find the best READY row that is NOT already leased and NOT already chosen-failed 191 best = 0 - 1 192 var rr: i64 = 0 193 while rr < cx[0] { 194 if dl_ready(cx, rr) == 1 { 195 if dl_lease_held(dir, an_id_at(cx, rr)) == 0 { 196 var take: i64 = 0 197 if best < 0 { take = 1 } else { 198 if wv[rr] > wv[best] { take = 1 } 199 if wv[rr] == wv[best] { if sz[rr] < sz[best] { take = 1 } } 200 } 201 if take == 1 { best = rr } 202 } 203 } 204 rr = rr + 1 205 } 206 if best < 0 { return 0 - 1 } // nothing ready+unleased remains 207 let bid: *u8 = an_id_at(cx, best) 208 let fd2: i64 = dl_try_lease(dir, bid) 209 if fd2 >= 0 { 210 var j: i64 = 0 211 while bid[j] != (0 as u8) { id_out[j] = bid[j]; j = j + 1 } 212 id_out[j] = 0 as u8 213 fd_out[0] = fd2 214 return fd2 215 } 216 // raced: someone leased `best` between our probe and our lock -> retry scan 217 guard = guard + 1 218 if guard > DL_MAGIC_4096 { done = 1 } // bounded give-up (no infinite spin) 219 } 220 return 0 - 1 221} 222 223// ---- single-write status emitter (the torn-line discipline) ---- 224// One record built into ONE buffer -> ONE locked atomic fa_appendz. Mirrors cn_emit_*. 225func dl_emit(path: *u8, verb: *u8, id: *u8, epoch: i64, ok: i64) -> i64 { 226 let buf: *u8 = sys_mmap(DL_REC_CAP + 16) 227 var o: i64 = 0 228 o = fa_cat(buf, o, "DISPATCH verb=\x00" as *u8) 229 o = fa_cat(buf, o, verb) 230 o = fa_cat(buf, o, " id=\x00" as *u8) 231 o = fa_cat(buf, o, id) 232 o = fa_cat(buf, o, " epoch=\x00" as *u8) 233 o = fa_catn(buf, o, epoch) 234 o = fa_cat(buf, o, " verdict=\x00" as *u8) 235 if ok == 1 { o = fa_cat(buf, o, "GREEN\x00" as *u8) } else { o = fa_cat(buf, o, "RED\x00" as *u8) } 236 return fa_appendz(path, buf, DL_REC_CAP) 237} 238 239func dl_p(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 } 240 241// thin CLI main: dispatch ONE ready rung over the live (or argv-overridden) queue, 242// leasing it under the (argv-overridden) lease dir, then RELEASE (CLI is a probe, not 243// a long-running worker -- it shows the pick + lease worked, then frees it). 244// argv[1] = queue path (default: knowledge/registry/assignment_queue.tsv) 245// argv[2] = lease dir (default: knowledge/status/leases) 246func main(argc: i64, argv: *i64) -> i64 { 247 var qp: *u8 = "knowledge/registry/assignment_queue.tsv" as *u8 248 var dir: *u8 = "knowledge/status/leases" as *u8 249 if argc >= 2 { qp = argv[1] as *u8 } 250 if argc >= 3 { dir = argv[2] as *u8 } 251 sys_mkdir(dir, 0x1ed) // ensure lease dir exists (0755); idempotent 252 let cx: *i64 = an_newcx() 253 let rows: i64 = an_load(qp, cx) 254 if rows <= 0 { 255 dl_p("DISPATCH verb=load id=NONE verdict=RED reason=queue-missing-or-empty\n" as *u8) 256 sys_exit(2) 257 return 2 258 } 259 let id_out: *u8 = sys_mmap(DL_IDCAP) 260 let fd_out: *i64 = sys_mmap(16) as *i64 261 let fd: i64 = dl_dispatch(cx, dir, id_out, fd_out) 262 if fd >= 0 { 263 dl_emit("knowledge/status/dispatch_lease.log\x00" as *u8, "leased\x00" as *u8, id_out, sys_now_realtime_sec(), 1) 264 dl_p("DISPATCH verb=leased id=" as *u8); dl_p(id_out); dl_p(" (CLI releases)\n" as *u8) 265 dl_release(dir, id_out, fd_out[0]) 266 sys_exit(0) 267 return 0 268 } 269 dl_emit("knowledge/status/dispatch_lease.log\x00" as *u8, "none\x00" as *u8, "NONE-READY\x00" as *u8, sys_now_realtime_sec(), 0) 270 dl_p("DISPATCH verb=none id=NONE-READY (all ready streams blocked or already leased)\n" as *u8) 271 sys_exit(1) 272 return 0 273}