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}