code wiki / (root) / nx_deploy_listen_gate.nx

nx_deploy_listen_gate.nx source

↩ module page · 243 lines · 15316 B

1// nx_deploy_listen_gate.nx -- THE REFEREE FOR THE DEPLOY LISTEN CHECK (debt 1786068492). 2// 3// WHAT IT PROVES, AND WHAT IT DELIBERATELY DOES NOT. The subject is md_probe_listen_edge_cfg: the 4// edge-triggered listener verification /api/deploy runs after it promotes and kills. The defect it exists to 5// catch is NOT "no check ran" -- a check ran for weeks -- it is that the check was LEVEL-TRIGGERED, so 6// "something is listening" was true both before and after the swap and a connect issued in the SIGKILL 7// teardown window greened on the OUTGOING process. This gate therefore does not merely ask "did it say 8// green"; it asks WHICH LISTENER the green describes. 9// 10// HERMETIC BY CONSTRUCTION: every fixture is a throwaway socket this gate binds itself, on a port it PROVES 11// free first, in a forked child it reaps. It never touches a serving daemon and writes NO scratch file at 12// all -- so it is idempotent by construction rather than by a teardown that a crash would skip. 13// license_tier: ORIGINAL No hw writes (Rule 26). 14import "nx_syscalls.nx" 15import "nx_gate_verdict.nx" 16import "_hdl_build/nx_mgmt_data.nx" 17 18// ---- fixture parameters. Rule 11: every number named, and every one DERIVED from a stated reason. ------- 19// The probe budget here is SYNTHETIC and deliberately tiny: the gate exercises the same code path at 20// millisecond timescales so the whole run costs ~1.5s instead of the production 30s. That is exactly why the 21// subject takes its budget as a PARAMETER. The PRODUCTION numbers are asserted separately, below, by the 22// derivation teeth -- so shrinking the fixture cannot quietly shrink what ships. 23const DLG_UP_TRIES: i64 = 6 // 6 x 100ms = 600ms up budget: 4x the bind delay below, so a healthy bind cannot lose on timing alone 24const DLG_UP_TICK_MS: i64 = 100 25const DLG_DOWN_TICK_MS: i64 = 20 // down window derives to 100/20 = 5 samples, mirroring the production derivation exactly 26const DLG_BIND_DELAY_MS: i64 = 150 // > one up-tick, so the probe MUST observe at least one REFUSED sample before the bind lands 27const DLG_HOLD_MS: i64 = 1200 // > the whole up budget, so the fixture cannot vanish mid-measurement 28const DLG_LINGER_MS: i64 = 1500 // fixture B outlives the probe: a child that exited early would free-run the port 29const DLG_PORT_BASE: i64 = 39701 // high, unassigned, above every port named in deploy_targets.conf 30const DLG_PORT_SCAN: i64 = 64 // bounded search; exhausting it is a PRECONDITION failure (SKIP), never a RED 31// BACKLOG SIZING IS LOAD-BEARING, NOT DECORATION -- measured 2026-08-21 as a 12s gate TIMEOUT. 32// md_tcp_alive CONNECTS and closes but never accept()s, and a closed client socket does NOT free its 33// slot in the accept queue -- the connection sits there until something accepts it. So once the queue 34// fills, every further connect gets its SYN dropped, nx_connect_bounded sees EINPROGRESS and polls out 35// the FULL NX_CONN_DEFAULT_MS (6000ms) per probe. At backlog=4 fixture C's 7 probes cost ~18s and the 36// gate died on the caller's deadline looking exactly like a hang in the subject. 37// DERIVED, not guessed: the worst fixture issues 1 condition-assert + (UP_TICK/DOWN_TICK) down-samples 38// + 1 up-sample against ONE listener. The tooth below binds this constant to that arithmetic so raising 39// a budget can never silently re-introduce the stall. 40const DLG_BACKLOG: i64 = 64 41 42// sockaddr_in field offsets. These are a STRUCT LAYOUT, not tunables -- naming them is what stops the 43// hand-counted-offset-beside-a-literal defect, where the layout and the indices drift apart silently. 44const DLG_SA_LEN: i64 = 16 45const DLG_SA_FAM_LO: i64 = 0 46const DLG_SA_FAM_HI: i64 = 1 47const DLG_SA_PORT_HI: i64 = 2 48const DLG_SA_PORT_LO: i64 = 3 49const DLG_SA_IP_A: i64 = 4 50const DLG_SA_IP_B: i64 = 5 51const DLG_SA_IP_C: i64 = 6 52const DLG_SA_IP_D: i64 = 7 53const DLG_SA_ADDR_END: i64 = 8 54const DLG_AF_INET_B: i64 = 2 55const DLG_BYTE_MASK: i64 = 0xff 56const DLG_PORT_SHIFT: i64 = 8 57const DLG_LOOPBACK_A: i64 = 127 58const DLG_LOOPBACK_D: i64 = 1 59 60const DLG_I64_BYTES: i64 = 8 // sizeof(i64): the slot table is MD_PC_SLOTS of these 61const DLG_WAIT_SCRATCH: i64 = 16 // wait4 status word scratch 62 63// the PRODUCTION defaults md_probe_conf must yield when no conf file is present 64const DLG_PROD_UP_TRIES: i64 = 10 65const DLG_PROD_UP_TICK_MS: i64 = 3000 66const DLG_PROD_DOWN_TICK_MS: i64 = 50 67// hc_deploy_one announces this bound to the operator in its own restart message ("guard respawns the NEW 68// binary ... <=15s"). The up budget must EXCEED it or healthy deploys roll back, so it is asserted as a 69// RELATIONSHIP below rather than as a duplicated literal. 70const DLG_GUARD_RESPAWN_BOUND_MS: i64 = 15000 71// the toolsapi health field from deploy_targets.conf. The port is spelled TWICE below -- once inside the 72// literal "port:18096" and once as this constant -- and that duplication is DELIBERATE: the tooth's whole 73// purpose is that the parser turns THAT EXACT TEXT into THAT NUMBER. Deriving the string from the constant 74// would make the parser agree with itself and the tooth would prove nothing. 75const DLG_TOOLSAPI_PORT: i64 = 18096 76 77// Bind+listen a throwaway on 127.0.0.1:<port>. Returns the fd, or -1. Mirrors md_tcp_alive's sockaddr layout 78// so the fixture and the ruler agree on the wire by construction. 79func dlg_listen(port: i64) -> i64 { 80 let fd: i64 = sys_socket(AF_INET, SOCK_STREAM, 0) 81 if fd < 0 { return 0 - 1 } 82 let sa: *u8 = sys_mmap(DLG_SA_LEN) 83 sa[DLG_SA_FAM_LO] = DLG_AF_INET_B as u8 84 sa[DLG_SA_FAM_HI] = 0 as u8 85 sa[DLG_SA_PORT_HI] = ((port >> DLG_PORT_SHIFT) & DLG_BYTE_MASK) as u8 86 sa[DLG_SA_PORT_LO] = (port & DLG_BYTE_MASK) as u8 87 sa[DLG_SA_IP_A] = DLG_LOOPBACK_A as u8 88 sa[DLG_SA_IP_B] = 0 as u8 89 sa[DLG_SA_IP_C] = 0 as u8 90 sa[DLG_SA_IP_D] = DLG_LOOPBACK_D as u8 91 var z: i64 = DLG_SA_ADDR_END 92 while z < DLG_SA_LEN { sa[z] = 0 as u8; z = z + 1 } 93 if sys_bind(fd, sa, DLG_SA_LEN) != 0 { sys_close(fd); return 0 - 1 } 94 if sys_listen(fd, DLG_BACKLOG) != 0 { sys_close(fd); return 0 - 1 } 95 return fd 96} 97 98// AAAAA A PROBE PORT YOU DID NOT VERIFY FREE IS NOT A CONTROL, IT IS A SECOND INSTANCE. Two independent 99// proofs are required and neither alone is sufficient: a CONNECT that is refused (nobody is accepting) AND a 100// BIND that succeeds (nobody holds the socket -- a bound-but-not-listening holder refuses connects too). 101func dlg_free_port(start: i64, tries: i64) -> i64 { 102 var p: i64 = start 103 var n: i64 = 0 104 while n < tries { 105 if md_tcp_alive(p) == 0 { 106 let fd: i64 = dlg_listen(p) 107 if fd >= 0 { sys_close(fd); return p } 108 } 109 p = p + 1 110 n = n + 1 111 } 112 return 0 - 1 113} 114 115func dlg_cfg(out: *i64, up_tries: i64, up_tick: i64, down_tick: i64) -> i64 { 116 out[MD_PC_UP_TRIES] = up_tries 117 out[MD_PC_UP_TICK_MS] = up_tick 118 out[MD_PC_DOWN_TICK_MS] = down_tick 119 return 0 120} 121func dlg_slots() -> *i64 { return sys_mmap(DLG_I64_BYTES * MD_PC_SLOTS) as *i64 } 122 123func main() -> i64 { 124 let ctr: *i64 = gv_ctr() 125 gv_head("nx_deploy_listen_gate -- the deploy listener check must name WHICH listener it saw (debt 1786068492)" as *u8) 126 127 let cfg: *i64 = dlg_slots() 128 dlg_cfg(cfg, DLG_UP_TRIES, DLG_UP_TICK_MS, DLG_DOWN_TICK_MS) 129 130 // ---- PRECONDITION: three genuinely free ports. Exhausting the scan says nothing about the subject, so 131 // it must SKIP, never RED -- an axis that cannot see must abstain, not acquit. 132 let pa: i64 = dlg_free_port(DLG_PORT_BASE, DLG_PORT_SCAN) 133 let pb: i64 = dlg_free_port(pa + 1, DLG_PORT_SCAN) 134 let pc: i64 = dlg_free_port(pb + 1, DLG_PORT_SCAN) 135 var gotports: i64 = 0 136 if pa > 0 { if pb > pa { if pc > pb { gotports = 1 } } } 137 if gv_need("three verified-free loopback ports for the fixtures" as *u8, gotports, ctr) == 0 { 138 let rcs: i64 = gv_verdict("DEPLOY-LISTEN-GATE" as *u8, ctr, "" as *u8) 139 sys_exit(rcs) 140 return rcs 141 } 142 143 // ---- FIXTURE A -- A REAL HANDOFF: port is down, then an incoming process binds it. Expect VERIFIED. 144 let kida: i64 = sys_fork() 145 if kida == 0 { 146 sys_sleep_ms(DLG_BIND_DELAY_MS) 147 let f: i64 = dlg_listen(pa) 148 sys_sleep_ms(DLG_HOLD_MS) 149 if f >= 0 { sys_close(f) } 150 sys_exit(0) 151 } 152 let a_down_at_start: i64 = md_tcp_alive(pa) // ASSERT THE FIXTURE REACHED ITS CONDITION, not just its outcome 153 let ra: i64 = md_probe_listen_edge_cfg(pa, cfg) 154 let a_up_at_end: i64 = md_tcp_alive(pa) 155 let wsa: *i64 = sys_mmap(DLG_WAIT_SCRATCH) as *i64 156 sys_wait4(kida, wsa, 0) 157 158 // ---- FIXTURE B -- THE EXACT OBSERVED FAILURE: the incoming process STARTS AND NEVER LISTENS. This is the 159 // neg-control the whole debt row describes: the new binary could not bind and exited, the guard respawned 160 // it, and the service stayed down with a perfectly healthy binary on disk. Expect NEVER_UP + rollback. 161 let kidb: i64 = sys_fork() 162 if kidb == 0 { 163 sys_sleep_ms(DLG_LINGER_MS) // alive as a PROCESS throughout, never a LISTENER: a process-existence check would have passed it 164 sys_exit(0) 165 } 166 let b_down_at_start: i64 = md_tcp_alive(pb) 167 let rb: i64 = md_probe_listen_edge_cfg(pb, cfg) 168 let wsb: *i64 = sys_mmap(DLG_WAIT_SCRATCH) as *i64 169 sys_wait4(kidb, wsb, 0) 170 171 // ---- FIXTURE C -- THE OVER-CLAIM CONTROL: a listener that is up the WHOLE time and never goes down. This 172 // is the outgoing process that never died, and it is the case the OLD level-triggered probe reported as a 173 // full green. Expect UNVERIFIED -- healthy-looking, provenance unproven -- and explicitly NOT VERIFIED. 174 let fc: i64 = dlg_listen(pc) 175 let c_up_at_start: i64 = md_tcp_alive(pc) 176 let rcc: i64 = md_probe_listen_edge_cfg(pc, cfg) 177 if fc >= 0 { sys_close(fc) } 178 179 // ---- ANTI-VACUITY FIRST. A probe that returns one constant for every input passes every single-outcome 180 // assertion below it. AAAAAA A CHECKER THAT ANSWERS THE SAME THING FOR EVERYTHING DISCRIMINATES NOTHING -- 181 // so the first tooth is that the three fixtures produced three DIFFERENT answers. 182 var distinct: i64 = 0 183 if ra != rb { if rb != rcc { if ra != rcc { distinct = 1 } } } 184 gv_check("anti-vacuity: three fixtures yield three DISTINCT outcomes" as *u8, distinct, ctr) 185 186 // ---- the fixtures genuinely reached their conditions (a fixture the defect cannot fail is not a test) 187 gv_check("fixture-A reached its condition: port was REFUSED before the incoming bind" as *u8, a_down_at_start == 0, ctr) 188 gv_check("fixture-A reached its condition: port was ACCEPTING when the probe returned" as *u8, a_up_at_end == 1, ctr) 189 gv_check("fixture-B reached its condition: port never accepted at any point" as *u8, b_down_at_start == 0, ctr) 190 gv_check("fixture-C reached its condition: a real listener was bound and accepting first" as *u8, c_up_at_start == 1, ctr) 191 192 // ---- the three outcomes themselves 193 gv_check("a witnessed handoff (down then up) reports VERIFIED" as *u8, ra == MD_HP_VERIFIED, ctr) 194 gv_check("a process that starts and never listens reports NEVER_UP -- the deploy rolls back" as *u8, rb == MD_HP_NEVER_UP, ctr) 195 gv_check("a listener that never dropped reports UNVERIFIED, NOT a full green" as *u8, rcc == MD_HP_UNVERIFIED, ctr) 196 gv_check("neg-control-never-claims-VERIFIED-without-witnessing-the-socket-change-hands" as *u8, rcc != MD_HP_VERIFIED, ctr) 197 gv_check("neg-control-a-healthy-deploy-is-never-rolled-back" as *u8, ra != MD_HP_NEVER_UP, ctr) 198 gv_check("neg-control-an-unwitnessed-handoff-is-never-rolled-back-either" as *u8, rcc != MD_HP_NEVER_UP, ctr) 199 200 // ---- BITE-PROVEN: fires on the bad input, silent on the good one. Without this pair a detector that 201 // refuses everything scores full marks on every negative test above. 202 gv_bite("neg-control-NEVER_UP-fires-only-on-a-process-that-never-bound" as *u8, rb == MD_HP_NEVER_UP, ra == MD_HP_NEVER_UP, ctr) 203 gv_bite("neg-control-UNVERIFIED-fires-only-when-no-handoff-was-witnessed" as *u8, rcc == MD_HP_UNVERIFIED, ra == MD_HP_UNVERIFIED, ctr) 204 205 // ---- THE PRODUCTION NUMBERS. The fixtures above run on a synthetic budget, so these teeth are what stop 206 // a shrunken fixture from quietly shrinking what ships. 207 let dflt: *i64 = dlg_slots() 208 md_probe_conf(dflt) 209 var prod_ok: i64 = 0 210 if dflt[MD_PC_UP_TRIES] > 0 { if dflt[MD_PC_UP_TICK_MS] > 0 { prod_ok = 1 } } 211 gv_check("the production up budget is positive on every axis (a zero tick spins, a zero try-count is an unconditional pass)" as *u8, prod_ok, ctr) 212 // THE REGRESSION TOOTH, earned the hard way 2026-08-21. The down-watch must default OFF: it costs the 213 // deploy watchdog ~one up-tick of LIFE before it can write any verdict, and measured on the live 214 // control plane the watchdog is KILLED inside that window -- which silently disarms the never-brick 215 // auto-rollback this whole path exists to provide. Off-by-default IS the safety property, so it gets a 216 // tooth rather than a comment. The fixtures above deliberately run with the watch ENABLED, so both 217 // states of the knob are exercised and neither is assumed. 218 gv_check("neg-control-the-down-watch-defaults-OFF-so-it-cannot-outlive-the-watchdog-verdict" as *u8, dflt[MD_PC_DOWN_TICK_MS] == 0, ctr) 219 let prod_budget_ms: i64 = dflt[MD_PC_UP_TRIES] * dflt[MD_PC_UP_TICK_MS] 220 gv_check("the up budget exceeds the guard-respawn bound hc_deploy_one announces" as *u8, prod_budget_ms > DLG_GUARD_RESPAWN_BOUND_MS, ctr) 221 222 // the down window is DERIVED as exactly one up-tick -- one calibrated quantity, never two that can drift 223 let synth: *i64 = dlg_slots() 224 dlg_cfg(synth, DLG_PROD_UP_TRIES, DLG_PROD_UP_TICK_MS, DLG_PROD_DOWN_TICK_MS) 225 gv_check("the down window derives to exactly one up-tick" as *u8, md_probe_down_tries(synth) == (DLG_PROD_UP_TICK_MS / DLG_PROD_DOWN_TICK_MS), ctr) 226 let zc: *i64 = dlg_slots() 227 dlg_cfg(zc, DLG_PROD_UP_TRIES, DLG_PROD_UP_TICK_MS, 0) 228 gv_check("neg-control-a-zero-down-tick-cannot-divide-by-zero-or-spin" as *u8, md_probe_down_tries(zc) == 1, ctr) 229 230 // The fixture backlog must exceed every probe one fixture issues against one listener, or the queue 231 // fills and each further connect polls out the whole connect budget -- a stall that reads as a hang in 232 // the SUBJECT, which is how this gate first failed. Bound to the ARITHMETIC, not a remembered number. 233 gv_check("the fixture backlog exceeds the probes one fixture issues against one listener" as *u8, DLG_BACKLOG > ((DLG_UP_TICK_MS / DLG_DOWN_TICK_MS) + DLG_UP_TRIES + 2), ctr) 234 235 // the port:<N> grammar the deploy row actually carries, and its rejection of anything else 236 gv_check("the port grammar parses the toolsapi row's health field" as *u8, md_url_port("port:18096" as *u8) == DLG_TOOLSAPI_PORT, ctr) 237 gv_check("neg-control-an-http-health-url-is-NOT-taken-as-a-port-row" as *u8, md_url_is_port("https://example.invalid/api/" as *u8) == 0, ctr) 238 gv_check("a port row IS recognised as one" as *u8, md_url_is_port("port:18096" as *u8) == 1, ctr) 239 240 let rc: i64 = gv_verdict("DEPLOY-LISTEN-GATE" as *u8, ctr, "the listener check names WHICH listener it saw; a start-but-never-listen deploy is caught and rolled back" as *u8) 241 sys_exit(rc) 242 return rc 243}