code wiki / (root) / nx_ts_slot_router_gate.nx

nx_ts_slot_router_gate.nx source

↩ module page · 419 lines · 19047 B

1// nx_ts_slot_router_gate.nx -- TS3: A FLIP THE CLIENT CANNOT SEE, AND A COLD SLOT IT REFUSES. 2// 3// /compare/trafficsafety rung TS3, accept rule taken VERBATIM from trafficsafety.plan and not 4// re-invented here: "a flip changes which slot answers with zero failed requests across the flip, a 5// slot that fails its warm-up probe is NEVER flipped to, and the front door's own uptime is unbroken 6// across the whole exercise." 7// 8// ALL THREE CONJUNCTS ARE TESTED SEPARATELY AND EACH NAMES ITSELF, because a compound assertion that 9// will not name its failing conjunct is a false-alarm generator -- the reader always guesses the 10// alarming third. 11// 12// THE SUBJECT IS THE REAL ROUTER BINARY, fork+exec'd, not a function called in-process. And the gate 13// plays OWNER of the front listener, handing it over with ts_handoff_publish, which does double duty: 14// it is a deterministic readiness edge (the publish returns only once the router asked for the 15// socket) AND it demonstrates the property TS3's own risk row demands -- that the one component which 16// must never restart is itself replaceable without a gap, which is why TS3 depends on TS1. 17// 18// THE COLD-SLOT CONTROL IS A REAL DEATH, NOT A FICTION. Slot A's server is killed AND its listening 19// socket closed, so the port genuinely refuses connections; only then is a flip back to A attempted. 20// A guard that gates a destructive action must be wrong in the direction of doing nothing, and moving 21// live traffic onto a dead slot is the destructive action here -- so the control asserts three things, 22// not one: that the flip was REFUSED BY NAME, that the active slot did not move, and that the client 23// still reaches the healthy slot afterwards. A refusal that quietly parked traffic nowhere would pass 24// the first and fail the third. 25// 26// SAFETY: loopback only; every port is FOUND by a bind that would have failed had it been occupied. 27// Nothing live is touched -- this router is standalone by design and fronts only the gate's own two 28// throwaway slot servers. Fixtures live under /tmp/<gate>/ and setup clears them, so a run cannot 29// inherit a previous run's state file and report on it. 30// expect_exit: 0 license_tier: ORIGINAL No hw writes (Rule 26). 31import "nx_syscalls.nx" 32import "nx_http_server.nx" 33import "nx_ts_slot_lib.nx" 34import "nx_gate_emit_lib.nx" 35import "nx_gate_verdict.nx" 36 37const TG_NUM_SCRATCH: i64 = 24 38const TG_ASCII_ZERO: i64 = 48 39const TG_B10: i64 = 10 40const TG_SA_BYTES: i64 = 16 41// sockaddr_in FIELD OFFSETS, named so a reader checks them against the platform layout instead of 42// meeting bare indices: sin_family at 0, sin_port at 2 (big-endian), sin_addr at 4. 43const TG_SA_PORT_OFF: i64 = 2 44const TG_SA_ADDR_OFF: i64 = 4 45const TG_ADDR_BYTES: i64 = 4 46const TG_BYTE_RADIX: i64 = 256 47const TG_OUT_BYTES: i64 = 128 48const TG_LOOPBACK_A: i64 = 127 49const TG_LOOPBACK_D: i64 = 1 50const TG_PORT_BASE: i64 = 39700 51const TG_PORT_TRIES: i64 = 64 52const TG_PORT_NONE: i64 = 0 - 1 53const TG_TRUE: i64 = 1 54const TG_FALSE: i64 = 0 55const TG_BUF: i64 = 4096 56const TG_ARGV_BYTES: i64 = 64 57const TG_SIG_TERM: i64 = 15 58const TG_ARGC_SUBJECT: i64 = 2 59// fcntl F_SETFD / FD_CLOEXEC, and the argv slots of the spawned router, named rather than left as 60// bare indices at the call site. 61const TG_F_SETFD: i64 = 2 62const TG_FD_CLOEXEC: i64 = 1 63const TG_AV_FRONT: i64 = 1 64const TG_AV_SLOTA: i64 = 2 65const TG_AV_SLOTB: i64 = 3 66const TG_AV_STATE: i64 = 4 67const TG_AV_SOCK: i64 = 5 68const TG_AV_END: i64 = 6 69// Exit code a child uses when execve itself failed -- an absent artifact must NAME itself rather than 70// surface to the caller as a dead server. 127 is the shell's own convention for not-found. 71const TG_RC_EXEC_FAILED: i64 = 127 72const TG_DEADLINE_S: i64 = ACCEPT_TMO_S 73const TG_MARK_A: i64 = 65 74const TG_MARK_B: i64 = 66 75const TG_PROBE: i64 = 80 76// The request stream per phase is taken from the router's own backlog rather than picked, so the 77// accept queue is exercised at its own scale on both sides of the flip. 78const TG_REQS: i64 = TSR_BACKLOG 79 80const TG_DIR: *u8 = "/tmp/nx_ts_slot_router_gate" as *u8 81const TG_STATE: *u8 = "/tmp/nx_ts_slot_router_gate/slots.conf" as *u8 82const TG_SOCK: *u8 = "/tmp/nx_ts_slot_router_gate/front.sock" as *u8 83const TG_DEFAULT_ROUTER: *u8 = "./nx_ts_slot_router.elf" as *u8 84 85func g_putn(v: i64) -> i64 { 86 if v == 0 { sys_write(1, "0" as *u8, 1); return 0 } 87 var m: i64 = v 88 if m < 0 { sys_write(1, "-" as *u8, 1); m = 0 - m } 89 let d: *u8 = sys_mmap(TG_NUM_SCRATCH); var k: i64 = 0 90 while m > 0 { d[k] = ((TG_ASCII_ZERO + (m - (m / TG_B10) * TG_B10)) as u8); m = m / TG_B10; k = k + 1 } 91 var j: i64 = k - 1 92 while j >= 0 { sys_write(1, ((d as i64)+j) as *u8, 1); j = j - 1 } 93 return 0 94} 95 96func gq(label: *u8, got: i64, want: i64, ctr: *i64) -> i64 { 97 var c: i64 = 0 98 if got == want { c = 1 } 99 let r: i64 = gv_check(label, c, ctr) 100 if c == 0 { 101 g_puts(" got=" as *u8); g_putn(got) 102 g_puts(" want=" as *u8); g_putn(want) 103 g_puts("\n" as *u8) 104 } 105 return r 106} 107 108func tg_sa(sa: *u8, port: i64) -> i64 { 109 var i: i64 = 0 110 while i < TG_SA_BYTES { sa[i] = 0; i = i + 1 } 111 // The zero-fill above already cleared every other octet, so only the non-zero lanes are written. 112 sa[0] = AF_INET 113 sa[TG_SA_PORT_OFF] = port / TG_BYTE_RADIX 114 sa[TG_SA_PORT_OFF + 1] = port % TG_BYTE_RADIX 115 sa[TG_SA_ADDR_OFF] = TG_LOOPBACK_A 116 sa[TG_SA_ADDR_OFF + TG_ADDR_BYTES - 1] = TG_LOOPBACK_D 117 return 0 118} 119 120func tg_listen(port: i64, reuse: i64) -> i64 { 121 let fd: i64 = sys_socket(AF_INET, SOCK_STREAM, 0) 122 if fd < 0 { return fd } 123 if reuse == TG_TRUE { 124 let ra: *u8 = sys_mmap(SCM_U32_BYTES) 125 var z: i64 = 0 126 while z < SCM_U32_BYTES { ra[z] = 0 as u8; z = z + 1 } 127 ra[0] = 1 128 sys_setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, ra, SCM_U32_BYTES) 129 } 130 // FD_CLOEXEC, AND THIS GATE LEARNED IT THE HARD WAY RATHER THAN INHERITING IT AS ADVICE. 131 // Without it the router -- which is fork+EXECVE'd -- inherits these slot listeners, so killing a 132 // slot server does NOT make its port dead: the router's inherited copy keeps it LISTENING, the 133 // cold-slot probe connects successfully and then blocks until its socket deadline. MEASURED: the 134 // neg-control still reported COLD and T05/T06 still passed, but by TIMEOUT rather than by 135 // refusal -- a right answer for the wrong reason, which is the easiest false proof to accept 136 // because the verdict vector looks exactly correct. It cost 30s per probe and hung the run. 137 // This is the estate's own port-hostage class (nx_http_server_listen carries the same line for 138 // the same reason), reproduced inside a gate written by someone who had just read that law. 139 // fcntl(fd, F_SETFD=2, FD_CLOEXEC=1). Fork-only children still inherit, so the slot servers work. 140 __syscall(72, fd, TG_F_SETFD, TG_FD_CLOEXEC, 0, 0, 0) 141 let sa: *u8 = sys_mmap(TG_SA_BYTES) 142 tg_sa(sa, port) 143 if sys_bind(fd, sa, TG_SA_BYTES) < 0 { sys_close(fd); return 0 - 1 } 144 if sys_listen(fd, TSR_BACKLOG) < 0 { sys_close(fd); return 0 - 1 } 145 return fd 146} 147 148func tg_find_port(from: i64) -> i64 { 149 var t: i64 = 0 150 while t < TG_PORT_TRIES { 151 let cand: i64 = from + t 152 let fd: i64 = tg_listen(cand, TG_FALSE) 153 if fd >= 0 { sys_close(fd); return cand } 154 t = t + 1 155 } 156 return TG_PORT_NONE 157} 158 159// A BACKEND SLOT. Answers every request with its own identity byte, so the client can tell WHICH slot 160// served it -- the flip is measured by who answered, never by reading the router's own log. 161func tg_slot_server(lfd: i64, marker: i64) -> i64 { 162 sys_alarm(TG_DEADLINE_S) 163 let b: *u8 = sys_mmap(TG_BUF) 164 var go: i64 = 1 165 while go == 1 { 166 let c: i64 = sys_accept(lfd) 167 if c < 0 { go = 0 } else { 168 sys_set_socket_timeout(c, TG_DEADLINE_S) 169 let n: i64 = sys_read(c, b, TG_BUF) 170 if n > 0 { b[0] = marker as u8; sys_write(c, b, 1) } 171 sys_close(c) 172 } 173 } 174 return 0 175} 176 177// One client request THROUGH the front door. Returns the identity byte of whichever slot answered, 178// or -1 when the request failed outright. 179func tg_ask(front: i64) -> i64 { 180 let sa: *u8 = sys_mmap(TG_SA_BYTES) 181 tg_sa(sa, front) 182 let fd: i64 = sys_socket(AF_INET, SOCK_STREAM, 0) 183 if fd < 0 { return 0 - 1 } 184 sys_set_socket_timeout(fd, TG_DEADLINE_S) 185 if sys_connect(fd, sa, TG_SA_BYTES) < 0 { sys_close(fd); return 0 - 1 } 186 let b: *u8 = sys_mmap(TG_BUF) 187 b[0] = TG_PROBE as u8 188 if sys_write(fd, b, 1) != 1 { sys_close(fd); return 0 - 1 } 189 let r: i64 = sys_read(fd, b, TG_BUF) 190 sys_close(fd) 191 if r <= 0 { return 0 - 1 } 192 return b[0] as i64 193} 194 195// Drive a stream and report how many were answered by `want`, plus how many FAILED outright. 196func tg_drive(front: i64, want: i64, out: *i64) -> i64 { 197 var hit: i64 = 0 198 var fail: i64 = 0 199 var i: i64 = 0 200 while i < TG_REQS { 201 let m: i64 = tg_ask(front) 202 if m < 0 { fail = fail + 1 } else { if m == want { hit = hit + 1 } } 203 i = i + 1 204 } 205 out[0] = hit 206 out[1] = fail 207 return hit 208} 209 210func tg_write_state(slot: i64) -> i64 { 211 let b: *u8 = sys_mmap(TG_BUF) 212 var o: i64 = 0 213 let k: *u8 = "active_slot " as *u8 214 var i: i64 = 0 215 while k[i] != (0 as u8) { b[o] = k[i]; o = o + 1; i = i + 1 } 216 b[o] = (TG_ASCII_ZERO + slot) as u8 217 o = o + 1 218 b[o] = 10 as u8 219 o = o + 1 220 let fd: i64 = sys_openat_wr(TG_STATE, MODE_0644) 221 if fd < 0 { return 0 - 1 } 222 sys_write(fd, b, o) 223 sys_fsync(fd) 224 sys_close(fd) 225 return 0 226} 227 228func tg_spawn_router(router: *u8, front: i64, pa: i64, pb: i64) -> i64 { 229 let f: *u8 = sys_mmap(TG_NUM_SCRATCH) 230 let a: *u8 = sys_mmap(TG_NUM_SCRATCH) 231 let bb: *u8 = sys_mmap(TG_NUM_SCRATCH) 232 tsr_itoa(front, f) 233 tsr_itoa(pa, a) 234 tsr_itoa(pb, bb) 235 let pid: i64 = sys_fork() 236 if pid != 0 { return pid } 237 let av: *i64 = (sys_mmap(TG_ARGV_BYTES)) as *i64 238 av[0] = router as i64 239 av[TG_AV_FRONT] = f as i64 240 av[TG_AV_SLOTA] = a as i64 241 av[TG_AV_SLOTB] = bb as i64 242 av[TG_AV_STATE] = TG_STATE as i64 243 av[TG_AV_SOCK] = TG_SOCK as i64 244 av[TG_AV_END] = 0 245 let ev: *i64 = (sys_mmap(TG_ARGV_BYTES)) as *i64 246 ev[0] = 0 247 sys_execve(router, av, ev) 248 sys_exit(TG_RC_EXEC_FAILED) 249 return 0 250} 251 252func main(argc: i64, argv: **u8) -> i64 { 253 let ctr: *i64 = gv_ctr() 254 gv_head("nx_ts_slot_router_gate -- TS3: a flip the client cannot see, a cold slot it refuses" as *u8) 255 256 var router: *u8 = TG_DEFAULT_ROUTER 257 if argc >= TG_ARGC_SUBJECT { router = argv[1] as *u8 } 258 g_puts(" subject=" as *u8); g_puts(router); g_puts("\n" as *u8) 259 260 // SETUP CLEARS LEAKABLE STATE: a run must not inherit a previous run's state file and report on it. 261 sys_mkdir(TG_DIR, MODE_0755) 262 sys_unlinkat(TG_STATE) 263 sys_unlinkat(TG_SOCK) 264 265 let dl: *i64 = (sys_mmap(TG_OUT_BYTES)) as *i64 266 dl[0] = 0 267 let probe: *u8 = sys_read_file(router, dl) 268 var have: i64 = 0 269 if probe != (0 as *u8) { if dl[0] > 0 { have = 1 } } 270 if gv_need("the subject router binary exists and is readable" as *u8, have, ctr) == 0 { 271 let rcs: i64 = gv_verdict("TS-SLOT-ROUTER-GATE" as *u8, ctr, "each tooth states its own strength above" as *u8) 272 sys_exit(rcs) 273 return rcs 274 } 275 276 let pf: i64 = tg_find_port(TG_PORT_BASE) 277 var pa: i64 = TG_PORT_NONE 278 if pf != TG_PORT_NONE { pa = tg_find_port(pf + 1) } 279 var pb: i64 = TG_PORT_NONE 280 if pa != TG_PORT_NONE { pb = tg_find_port(pa + 1) } 281 var ports_ok: i64 = 0 282 if pf != TG_PORT_NONE { if pa != TG_PORT_NONE { if pb != TG_PORT_NONE { ports_ok = 1 } } } 283 if gv_need("three DISTINCT loopback ports proven free by a bind without SO_REUSEADDR" as *u8, ports_ok, ctr) == 0 { 284 let rcs: i64 = gv_verdict("TS-SLOT-ROUTER-GATE" as *u8, ctr, "each tooth states its own strength above" as *u8) 285 sys_exit(rcs) 286 return rcs 287 } 288 289 let ports: *i64 = (sys_mmap(TG_OUT_BYTES)) as *i64 290 ports[TSR_SLOT_A] = pa 291 ports[TSR_SLOT_B] = pb 292 293 // ---- two backend slots, listeners created by the parent so no child races to bind 294 let la: i64 = tg_listen(pa, TG_TRUE) 295 let lb: i64 = tg_listen(pb, TG_TRUE) 296 // EACH SLOT CHILD CLOSES ITS SIBLING'S LISTENER, and this is load-bearing rather than tidy. 297 // fork copies every descriptor, so slot B's process would otherwise hold a live copy of slot A's 298 // listening socket -- and then KILLING slot A does not make port A dead, because B is still 299 // holding it open. The cold-slot control would connect successfully, stall to its deadline, and 300 // report COLD for the wrong reason. FD_CLOEXEC alone does NOT cover this: these children fork 301 // WITHOUT exec, so only an explicit close reaches them. Measured both ways -- with the close the 302 // port refuses immediately, without it the probe took its full socket deadline twice over. 303 let sa_pid: i64 = sys_fork() 304 if sa_pid == 0 { sys_close(lb); sys_exit(tg_slot_server(la, TG_MARK_A)) } 305 let sb_pid: i64 = sys_fork() 306 if sb_pid == 0 { sys_close(la); sys_exit(tg_slot_server(lb, TG_MARK_B)) } 307 308 tg_write_state(TSR_SLOT_A) 309 310 // ---- the front door. The GATE owns its listener and hands it over, which is both a deterministic 311 // readiness edge and a demonstration that the front door is itself hot-replaceable (TS1). 312 let vb: *i64 = (sys_mmap(TG_OUT_BYTES)) as *i64 313 vb[0] = 0 314 let rv: i64 = ts_handoff_open(TG_SOCK, vb) 315 var front_ok: i64 = 0 316 var rpid: i64 = 0 - 1 317 var frontl: i64 = 0 - 1 318 if rv >= 0 { 319 sys_set_socket_timeout(rv, TG_DEADLINE_S) 320 frontl = tg_listen(pf, TG_FALSE) 321 if frontl >= 0 { 322 rpid = tg_spawn_router(router, pf, pa, pb) 323 if ts_handoff_publish(rv, frontl, vb) >= 0 { front_ok = 1 } 324 } 325 } 326 if gv_need("the front door started and took the listening socket from its owner" as *u8, front_ok, ctr) == 0 { 327 if rpid > 0 { nx_kill(rpid, TG_SIG_TERM) } 328 nx_kill(sa_pid, TG_SIG_TERM) 329 nx_kill(sb_pid, TG_SIG_TERM) 330 let rcs: i64 = gv_verdict("TS-SLOT-ROUTER-GATE" as *u8, ctr, "each tooth states its own strength above" as *u8) 331 sys_exit(rcs) 332 return rcs 333 } 334 335 var att: i64 = 0 336 var fails: i64 = 0 337 let o: *i64 = (sys_mmap(TG_OUT_BYTES)) as *i64 338 339 // ---- PHASE 1: baseline, slot A is active 340 let hitA: i64 = tg_drive(pf, TG_MARK_A, o) 341 att = att + TG_REQS 342 fails = fails + o[1] 343 g_puts(" PHASE1 active=A answered_by_A=" as *u8); g_putn(hitA) 344 g_puts(" failed=" as *u8); g_putn(o[1]); g_puts("\n" as *u8) 345 // ANTI-VACUITY FIRST: a flip test in which the front door never served is UNOBSERVED, not a pass. 346 gq("T01 anti-vacuity the front door really served before any flip" as *u8, hitA, TG_REQS, ctr) 347 348 // ---- PHASE 2: flip to B and re-drive 349 let f1: i64 = ts_slot_flip(TG_STATE, ports, TSR_SLOT_B) 350 gq("T02 a flip onto a WARM slot is accepted" as *u8, f1, TSR_FLIP_OK, ctr) 351 gq("T03 and the state plane now names the new slot" as *u8, ts_slot_active(TG_STATE), TSR_SLOT_B, ctr) 352 let hitB: i64 = tg_drive(pf, TG_MARK_B, o) 353 att = att + TG_REQS 354 fails = fails + o[1] 355 g_puts(" PHASE2 active=B answered_by_B=" as *u8); g_putn(hitB) 356 g_puts(" failed=" as *u8); g_putn(o[1]); g_puts("\n" as *u8) 357 gq("T04 THE FLIP IS REAL every request after it is answered by the OTHER slot" as *u8, hitB, TG_REQS, ctr) 358 359 // ---- PHASE 3: NEG-CONTROL. Kill slot A for real, then try to flip back onto it. 360 nx_kill(sa_pid, TG_SIG_TERM) 361 let st: *i64 = (sys_mmap(TG_OUT_BYTES)) as *i64 362 st[0] = 0 363 sys_wait4(sa_pid, st, 0) 364 sys_close(la) 365 // The control must be a REAL death: prove the port refuses before believing the refusal that follows. 366 // WHICH KIND OF COLD, asserted BEFORE the probe verdict is believed. A port that is still bound 367 // by some other process accepts the connection and then stalls until the socket deadline -- the 368 // probe would still answer "cold", but by TIMEOUT rather than by refusal, and a tooth that cannot 369 // tell those apart passes for the wrong reason. This one pins the failure mode. 370 var conn_refused: i64 = 0 371 let craw: i64 = tsr_dial(pa) 372 if craw < 0 { conn_refused = 1 } else { sys_close(craw) } 373 gq("T05 neg-control-attribution the killed slot's port REFUSES connections, so cold means refused and not merely slow" as *u8, 374 conn_refused, 1, ctr) 375 let coldp: i64 = ts_slot_probe(pa) 376 gq("T05b neg-control-attribution and the warm-up probe therefore reads it as cold" as *u8, coldp, 0, ctr) 377 let f2: i64 = ts_slot_flip(TG_STATE, ports, TSR_SLOT_A) 378 gq("T06 neg-control-cold-slot a flip onto a COLD slot is REFUSED BY NAME" as *u8, f2, TSR_FLIP_COLD, ctr) 379 gq("T07 neg-control-cold-slot the active slot did NOT move" as *u8, ts_slot_active(TG_STATE), TSR_SLOT_B, ctr) 380 // A refusal that quietly parked traffic nowhere would pass T06 and T07 and still be an outage. 381 let hitB2: i64 = tg_drive(pf, TG_MARK_B, o) 382 att = att + TG_REQS 383 fails = fails + o[1] 384 g_puts(" PHASE3 refused_flip active=B answered_by_B=" as *u8); g_putn(hitB2) 385 g_puts(" failed=" as *u8); g_putn(o[1]); g_puts("\n" as *u8) 386 gq("T08 and the client still reaches the HEALTHY slot after the refusal" as *u8, hitB2, TG_REQS, ctr) 387 388 // ---- THE ACCEPT RULE and the uptime conjunct 389 g_puts(" TOTAL attempted=" as *u8); g_putn(att) 390 g_puts(" failed=" as *u8); g_putn(fails) 391 g_puts(" front_pid=" as *u8); g_putn(rpid) 392 g_puts("\n" as *u8) 393 gq("T09 THE ACCEPT RULE zero failed requests across the whole exercise" as *u8, fails, 0, ctr) 394 // THE FRONT DOOR'S OWN UPTIME: it was never signalled, never restarted, and is still the same 395 // process -- nx_kill with signal 0 tests existence without delivering anything. 396 var alive: i64 = 0 397 if nx_kill(rpid, 0) == 0 { alive = 1 } 398 gq("T10 the front door was never restarted and is still the SAME live process" as *u8, alive, 1, ctr) 399 // BOTH SIGNALS PRESENT AT ONCE: a warm candidate flipped and a cold one did not, same run, same 400 // guard -- two runs that each isolate one outcome do not prove the guard discriminates. 401 var disc: i64 = 0 402 if f1 == TSR_FLIP_OK { if f2 == TSR_FLIP_COLD { disc = 1 } } 403 gq("T11 the warm-up guard DISCRIMINATES warm from cold in the same run" as *u8, disc, 1, ctr) 404 405 nx_kill(rpid, TG_SIG_TERM) 406 st[0] = 0 407 sys_wait4(rpid, st, 0) 408 gq("T12 and on TERM the front door DRAINED rather than being killed (TS2 inherited)" as *u8, 409 wait_term_signal(st[0]), 0, ctr) 410 nx_kill(sb_pid, TG_SIG_TERM) 411 sys_close(lb) 412 if frontl >= 0 { sys_close(frontl) } 413 if rv >= 0 { sys_close(rv) } 414 sys_unlinkat(TG_SOCK) 415 416 let rc: i64 = gv_verdict("TS-SLOT-ROUTER-GATE" as *u8, ctr, "each tooth states its own strength above" as *u8) 417 sys_exit(rc) 418 return rc 419}